125 125 | )
|
126 126 | }
|
127 127 | let (server_shutdown, server_shutdown_receiver) = tokio::sync::oneshot::channel();
|
128 128 | let (server_fut, server_addr) = run_server(server_shutdown_receiver).await;
|
129 129 | let server_handle = tokio::spawn(server_fut);
|
130 130 | tokio::time::sleep(Duration::from_millis(100)).await;
|
131 131 |
|
132 132 | let config = Config::builder()
|
133 133 | .with_test_defaults()
|
134 134 | .region(Region::new("us-east-1"))
|
135 + | .retry_config(RetryConfig::disabled())
|
135 136 | .timeout_config(
|
136 137 | TimeoutConfig::builder()
|
137 138 | .read_timeout(Duration::from_millis(300))
|
138 139 | .build(),
|
139 140 | )
|
140 141 | .endpoint_url(format!("http://{server_addr}"))
|
141 142 | .build();
|
142 143 | let client = Client::from_conf(config);
|
143 144 |
|
144 145 | if let Ok(result) = timeout(
|
145 146 | Duration::from_millis(1000),
|
146 147 | client.get_object().bucket("test").key("test").send(),
|
147 148 | )
|
148 149 | .await
|
149 150 | {
|
150 151 | match result {
|
151 152 | Ok(_) => panic!("should not have succeeded"),
|
152 153 | Err(err) => {
|
153 154 | let message = format!("{}", DisplayErrorContext(&err));
|
154 155 | let expected = "timeout: HTTP read timeout occurred after 300ms";
|
155 156 | assert!(
|
156 157 | message.contains(expected),
|
157 158 | "expected '{message}' to contain '{expected}'"
|
158 159 | );
|
159 160 | }
|
160 161 | }
|
161 162 | } else {
|
162 163 | panic!("the client didn't timeout");
|
163 164 | }
|
164 165 |
|
165 166 | server_shutdown.send(()).unwrap();
|
166 167 | server_handle.await.unwrap();
|
167 168 | }
|
168 169 |
|
169 170 | #[tokio::test]
|
170 171 | async fn test_connect_timeout() {
|
171 172 | let config = Config::builder()
|
172 173 | .with_test_defaults()
|
173 174 | .region(Region::new("us-east-1"))
|
175 + | .retry_config(RetryConfig::disabled())
|
174 176 | .timeout_config(
|
175 177 | TimeoutConfig::builder()
|
176 178 | .connect_timeout(Duration::from_millis(300))
|
177 179 | .build(),
|
178 180 | )
|
179 181 | .endpoint_url(
|
180 182 | // Emulate a connect timeout error by hitting an unroutable IP
|
181 183 | "http://172.255.255.0:18104",
|
182 184 | )
|
183 185 | .build();
|
184 186 | let client = Client::from_conf(config);
|
185 187 |
|
186 188 | if let Ok(result) = timeout(
|
187 189 | Duration::from_millis(1000),
|
188 190 | client.get_object().bucket("test").key("test").send(),
|
189 191 | )
|
190 192 | .await
|
191 193 | {
|
192 194 | match result {
|
193 195 | Ok(_) => panic!("should not have succeeded"),
|
194 196 | Err(err) => {
|
195 197 | let message = format!("{}", DisplayErrorContext(&err));
|
196 198 | let expected =
|
197 199 | "timeout: client error (Connect): HTTP connect timeout occurred after 300ms";
|
198 200 | assert!(
|
199 201 | message.contains(expected),
|
200 202 | "expected '{message}' to contain '{expected}'"
|
201 203 | );
|
202 204 | }
|
203 205 | }
|
204 206 | } else {
|
205 207 | panic!("the client didn't timeout");
|
206 208 | }
|
207 209 | }
|
210 + |
|
211 + | #[tokio::test]
|
212 + | #[expect(deprecated)]
|
213 + | async fn test_connect_timeout_enabled_by_default_with_new_behavior_version() {
|
214 + | use aws_smithy_runtime_api::client::behavior_version::BehaviorVersion;
|
215 + |
|
216 + | // With BehaviorVersion >= v2025_01_17, a 3.1s connect timeout is enabled by default
|
217 + | let config = Config::builder()
|
218 + | .behavior_version(BehaviorVersion::v2025_01_17())
|
219 + | .region(Region::new("us-east-1"))
|
220 + | .retry_config(RetryConfig::disabled())
|
221 + | .endpoint_url(
|
222 + | // Emulate a connect timeout error by hitting an unroutable IP
|
223 + | "http://172.255.255.0:18104",
|
224 + | )
|
225 + | .build();
|
226 + | let client = Client::from_conf(config);
|
227 + |
|
228 + | if let Ok(result) = timeout(
|
229 + | Duration::from_millis(5000),
|
230 + | client.get_object().bucket("test").key("test").send(),
|
231 + | )
|
232 + | .await
|
233 + | {
|
234 + | match result {
|
235 + | Ok(_) => panic!("should not have succeeded"),
|
236 + | Err(err) => {
|
237 + | let message = format!("{}", DisplayErrorContext(&err));
|
238 + | // Should timeout with the default 3.1s connect timeout
|
239 + | let expected = "HTTP connect timeout occurred after 3.1s";
|
240 + | assert!(
|
241 + | message.contains(expected),
|
242 + | "expected '{message}' to contain '{expected}'"
|
243 + | );
|
244 + | }
|
245 + | }
|
246 + | } else {
|
247 + | panic!("the client didn't timeout");
|
248 + | }
|
249 + | }
|
250 + |
|
251 + | #[tokio::test]
|
252 + | #[expect(deprecated)]
|
253 + | async fn test_old_behavior_version_has_no_default_connect_timeout() {
|
254 + | use aws_credential_types::Credentials;
|
255 + | use aws_smithy_runtime_api::client::behavior_version::BehaviorVersion;
|
256 + |
|
257 + | // With v2024_03_28 (older BMV), no default connect timeout should be set
|
258 + | let config = Config::builder()
|
259 + | .behavior_version(BehaviorVersion::v2024_03_28())
|
260 + | .region(Region::new("us-east-1"))
|
261 + | .credentials_provider(Credentials::for_tests())
|
262 + | .retry_config(RetryConfig::disabled())
|
263 + | .endpoint_url("http://172.255.255.0:18104") // Unroutable IP
|
264 + | .build();
|
265 + | let client = Client::from_conf(config);
|
266 + |
|
267 + | // The client should hang indefinitely without a timeout
|
268 + | // We wrap it in a short test timeout to verify it doesn't complete quickly
|
269 + | let result = timeout(
|
270 + | Duration::from_millis(500),
|
271 + | client.get_object().bucket("test").key("test").send(),
|
272 + | )
|
273 + | .await;
|
274 + |
|
275 + | // Should timeout at the test wrapper level (not at client level)
|
276 + | // This proves the client has no default connect timeout
|
277 + | assert!(
|
278 + | result.is_err(),
|
279 + | "client should hang without default timeout, causing test timeout"
|
280 + | );
|
281 + | }
|