Skip to content

Commit e10881d

Browse files
authored
Add tests for IO Error retries (#13627)
Often, HTTP requests don't fail due to server errors, but from spurious network errors such as connection resets. reqwest surfaces these as `io::Error`, and we have to handle their retrying separately. Companion PR: LukeMathWalker/wiremock-rs#159
1 parent 62ed17b commit e10881d

File tree

3 files changed

+162
-36
lines changed

3 files changed

+162
-36
lines changed

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ windows-core = { version = "0.59.0" }
189189
windows-registry = { version = "0.5.0" }
190190
windows-result = { version = "0.3.0" }
191191
windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_Registry"] }
192-
wiremock = { version = "0.6.2" }
192+
wiremock = { git = "https://github.com/astral-sh/wiremock-rs", rev = "b79b69f62521df9f83a54e866432397562eae789" }
193193
xz2 = { version = "0.1.7" }
194194
zip = { version = "2.2.3", default-features = false, features = ["deflate", "zstd", "bzip2", "lzma", "xz"] }
195195

crates/uv/tests/it/network.rs

Lines changed: 160 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,54 @@
1-
use std::env;
1+
use std::{env, io};
22

3-
use assert_fs::fixture::{FileWriteStr, PathChild};
3+
use assert_fs::fixture::{ChildPath, FileWriteStr, PathChild};
44
use http::StatusCode;
55
use serde_json::json;
66
use wiremock::matchers::method;
77
use wiremock::{Mock, MockServer, ResponseTemplate};
88

99
use crate::common::{TestContext, uv_snapshot};
1010

11-
/// Check the simple index error message when the server returns HTTP status 500, a retryable error.
12-
#[tokio::test]
13-
async fn simple_http_500() {
14-
let context = TestContext::new("3.12");
11+
fn connection_reset(_request: &wiremock::Request) -> io::Error {
12+
io::Error::new(io::ErrorKind::ConnectionReset, "Connection reset by peer")
13+
}
1514

15+
/// Answers with a retryable HTTP status 500.
16+
async fn http_error_server() -> (MockServer, String) {
1617
let server = MockServer::start().await;
1718
Mock::given(method("GET"))
1819
.respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR))
1920
.mount(&server)
2021
.await;
22+
23+
let mock_server_uri = server.uri();
24+
(server, mock_server_uri)
25+
}
26+
27+
/// Answers with a retryable connection reset IO error.
28+
async fn io_error_server() -> (MockServer, String) {
29+
let server = MockServer::start().await;
30+
Mock::given(method("GET"))
31+
.respond_with_err(connection_reset)
32+
.mount(&server)
33+
.await;
34+
2135
let mock_server_uri = server.uri();
36+
(server, mock_server_uri)
37+
}
38+
39+
/// Check the simple index error message when the server returns HTTP status 500, a retryable error.
40+
#[tokio::test]
41+
async fn simple_http_500() {
42+
let context = TestContext::new("3.12");
43+
44+
let (_server_drop_guard, mock_server_uri) = http_error_server().await;
2245

2346
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
2447
uv_snapshot!(filters, context
2548
.pip_install()
2649
.arg("tqdm")
2750
.arg("--index-url")
28-
.arg(server.uri()), @r"
51+
.arg(&mock_server_uri), @r"
2952
success: false
3053
exit_code: 2
3154
----- stdout -----
@@ -36,25 +59,46 @@ async fn simple_http_500() {
3659
");
3760
}
3861

62+
/// Check the simple index error message when the server returns a retryable IO error.
63+
#[tokio::test]
64+
async fn simple_io_err() {
65+
let context = TestContext::new("3.12");
66+
67+
let (_server_drop_guard, mock_server_uri) = io_error_server().await;
68+
69+
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
70+
uv_snapshot!(filters, context
71+
.pip_install()
72+
.arg("tqdm")
73+
.arg("--index-url")
74+
.arg(&mock_server_uri), @r"
75+
success: false
76+
exit_code: 2
77+
----- stdout -----
78+
79+
----- stderr -----
80+
error: Failed to fetch: `[SERVER]/tqdm/`
81+
Caused by: Request failed after 3 retries
82+
Caused by: error sending request for url ([SERVER]/tqdm/)
83+
Caused by: client error (SendRequest)
84+
Caused by: connection closed before message completed
85+
");
86+
}
87+
3988
/// Check the find links error message when the server returns HTTP status 500, a retryable error.
4089
#[tokio::test]
4190
async fn find_links_http_500() {
4291
let context = TestContext::new("3.12");
4392

44-
let server = MockServer::start().await;
45-
Mock::given(method("GET"))
46-
.respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR))
47-
.mount(&server)
48-
.await;
49-
let mock_server_uri = server.uri();
93+
let (_server_drop_guard, mock_server_uri) = http_error_server().await;
5094

5195
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
5296
uv_snapshot!(filters, context
5397
.pip_install()
5498
.arg("tqdm")
5599
.arg("--no-index")
56100
.arg("--find-links")
57-
.arg(server.uri()), @r"
101+
.arg(&mock_server_uri), @r"
58102
success: false
59103
exit_code: 2
60104
----- stdout -----
@@ -66,18 +110,41 @@ async fn find_links_http_500() {
66110
");
67111
}
68112

113+
/// Check the find links error message when the server returns a retryable IO error.
114+
#[tokio::test]
115+
async fn find_links_io_error() {
116+
let context = TestContext::new("3.12");
117+
118+
let (_server_drop_guard, mock_server_uri) = io_error_server().await;
119+
120+
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
121+
uv_snapshot!(filters, context
122+
.pip_install()
123+
.arg("tqdm")
124+
.arg("--no-index")
125+
.arg("--find-links")
126+
.arg(&mock_server_uri), @r"
127+
success: false
128+
exit_code: 2
129+
----- stdout -----
130+
131+
----- stderr -----
132+
error: Failed to read `--find-links` URL: [SERVER]/
133+
Caused by: Failed to fetch: `[SERVER]/`
134+
Caused by: Request failed after 3 retries
135+
Caused by: error sending request for url ([SERVER]/)
136+
Caused by: client error (SendRequest)
137+
Caused by: connection closed before message completed
138+
");
139+
}
140+
69141
/// Check the direct package URL error message when the server returns HTTP status 500, a retryable
70142
/// error.
71143
#[tokio::test]
72144
async fn direct_url_http_500() {
73145
let context = TestContext::new("3.12");
74146

75-
let server = MockServer::start().await;
76-
Mock::given(method("GET"))
77-
.respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR))
78-
.mount(&server)
79-
.await;
80-
let mock_server_uri = server.uri();
147+
let (_server_drop_guard, mock_server_uri) = http_error_server().await;
81148

82149
let tqdm_url = format!(
83150
"{mock_server_uri}/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl"
@@ -97,22 +164,35 @@ async fn direct_url_http_500() {
97164
");
98165
}
99166

100-
/// Check the Python install error message when the server returns HTTP status 500, a retryable
101-
/// error.
167+
/// Check the direct package URL error message when the server returns a retryable IO error.
102168
#[tokio::test]
103-
async fn python_install_http_500() {
104-
let context = TestContext::new("3.12")
105-
.with_filtered_python_keys()
106-
.with_filtered_exe_suffix()
107-
.with_managed_python_dirs();
169+
async fn direct_url_io_error() {
170+
let context = TestContext::new("3.12");
108171

109-
let server = MockServer::start().await;
110-
Mock::given(method("GET"))
111-
.respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR))
112-
.mount(&server)
113-
.await;
114-
let mock_server_uri = server.uri();
172+
let (_server_drop_guard, mock_server_uri) = io_error_server().await;
115173

174+
let tqdm_url = format!(
175+
"{mock_server_uri}/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl"
176+
);
177+
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
178+
uv_snapshot!(filters, context
179+
.pip_install()
180+
.arg(format!("tqdm @ {tqdm_url}")), @r"
181+
success: false
182+
exit_code: 1
183+
----- stdout -----
184+
185+
----- stderr -----
186+
× Failed to download `tqdm @ [SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl`
187+
├─▶ Failed to fetch: `[SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl`
188+
├─▶ Request failed after 3 retries
189+
├─▶ error sending request for url ([SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl)
190+
├─▶ client error (SendRequest)
191+
╰─▶ connection closed before message completed
192+
");
193+
}
194+
195+
fn write_python_downloads_json(context: &TestContext, mock_server_uri: &String) -> ChildPath {
116196
let python_downloads_json = context.temp_dir.child("python_downloads.json");
117197
let interpreter = json!({
118198
"cpython-3.10.0-darwin-aarch64-none": {
@@ -135,6 +215,21 @@ async fn python_install_http_500() {
135215
python_downloads_json
136216
.write_str(&serde_json::to_string(&interpreter).unwrap())
137217
.unwrap();
218+
python_downloads_json
219+
}
220+
221+
/// Check the Python install error message when the server returns HTTP status 500, a retryable
222+
/// error.
223+
#[tokio::test]
224+
async fn python_install_http_500() {
225+
let context = TestContext::new("3.12")
226+
.with_filtered_python_keys()
227+
.with_filtered_exe_suffix()
228+
.with_managed_python_dirs();
229+
230+
let (_server_drop_guard, mock_server_uri) = http_error_server().await;
231+
232+
let python_downloads_json = write_python_downloads_json(&context, &mock_server_uri);
138233

139234
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
140235
uv_snapshot!(filters, context
@@ -152,3 +247,35 @@ async fn python_install_http_500() {
152247
Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst)
153248
");
154249
}
250+
251+
/// Check the Python install error message when the server returns a retryable IO error.
252+
#[tokio::test]
253+
async fn python_install_io_error() {
254+
let context = TestContext::new("3.12")
255+
.with_filtered_python_keys()
256+
.with_filtered_exe_suffix()
257+
.with_managed_python_dirs();
258+
259+
let (_server_drop_guard, mock_server_uri) = io_error_server().await;
260+
261+
let python_downloads_json = write_python_downloads_json(&context, &mock_server_uri);
262+
263+
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
264+
uv_snapshot!(filters, context
265+
.python_install()
266+
.arg("cpython-3.10.0-darwin-aarch64-none")
267+
.arg("--python-downloads-json-url")
268+
.arg(python_downloads_json.path()), @r"
269+
success: false
270+
exit_code: 1
271+
----- stdout -----
272+
273+
----- stderr -----
274+
error: Failed to install cpython-3.10.0-macos-aarch64-none
275+
Caused by: Failed to download [SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst
276+
Caused by: Request failed after 3 retries
277+
Caused by: error sending request for url ([SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst)
278+
Caused by: client error (SendRequest)
279+
Caused by: connection closed before message completed
280+
");
281+
}

0 commit comments

Comments
 (0)