Skip to content

Commit ce49c26

Browse files
committed
Add tests for IO Error retries
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.
1 parent 93bbfd1 commit ce49c26

File tree

3 files changed

+159
-36
lines changed

3 files changed

+159
-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
@@ -187,7 +187,7 @@ windows-registry = { version = "0.5.0" }
187187
windows-result = { version = "0.3.0" }
188188
windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_Registry"] }
189189
winsafe = { version = "0.0.23", features = ["kernel"] }
190-
wiremock = { version = "0.6.2" }
190+
wiremock = { git = "https://github.com/konstin/wiremock-rs", rev = "131ad0a3e7a247c867fc290319047fc086995271" }
191191
xz2 = { version = "0.1.7" }
192192
zip = { version = "2.2.3", default-features = false, features = ["deflate"] }
193193

crates/uv/tests/it/network.rs

Lines changed: 157 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, 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, 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, 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, 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, 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, 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,50 @@ 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, mock_server_uri) = http_error_server().await;
231+
232+
let python_downloads_json = write_python_downloads_json(&context, &mock_server_uri);
233+
234+
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
235+
uv_snapshot!(filters, context
236+
.python_install()
237+
.arg("cpython-3.10.0-darwin-aarch64-none")
238+
.arg("--python-downloads-json-url")
239+
.arg(python_downloads_json.path()), @r"
240+
success: false
241+
exit_code: 1
242+
----- stdout -----
243+
244+
----- stderr -----
245+
error: Failed to install cpython-3.10.0-macos-aarch64-none
246+
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
247+
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)
248+
");
249+
}
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, mock_server_uri) = http_error_server().await;
260+
261+
let python_downloads_json = write_python_downloads_json(&context, &mock_server_uri);
138262

139263
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
140264
uv_snapshot!(filters, context

0 commit comments

Comments
 (0)