From 14f2e54c9c19b151bb106b740b27ff24f4063c32 Mon Sep 17 00:00:00 2001 From: Francisco Gouveia Date: Wed, 11 Feb 2026 22:56:56 +0000 Subject: [PATCH 1/3] fix(downloads): check correct response when resuming from partial (curl) --- src/download/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/download/mod.rs b/src/download/mod.rs index 09f6ac9951..d0351e8de1 100644 --- a/src/download/mod.rs +++ b/src/download/mod.rs @@ -538,14 +538,14 @@ mod curl { })?; } - // If we didn't get a 20x or 0 ("OK" for files) then return an error + // If we didn't get a 20x or 0 ("OK" for files) then return an error. + // If resuming a download, we need a 206, as a 200 would mean the server ignored + // the range header, resulting in corruption. let code = handle.response_code()?; - match code { - 0 | 200..=299 => {} - _ => { - return Err(DownloadError::HttpStatus(code).into()); - } - }; + match (resume_from > 0, code) { + (_, 0) | (true, 206) | (false, 200..=299) => {} + _ => return Err(DownloadError::HttpStatus(code).into()), + } Ok(()) }) From 13582d2e1ce6c79812b0d92b3c9915006680ad60 Mon Sep 17 00:00:00 2001 From: Francisco Gouveia Date: Wed, 11 Feb 2026 22:57:23 +0000 Subject: [PATCH 2/3] fix(downloads): check correct response when resuming from partial (reqwest) --- src/download/mod.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/download/mod.rs b/src/download/mod.rs index d0351e8de1..516cae49ca 100644 --- a/src/download/mod.rs +++ b/src/download/mod.rs @@ -587,9 +587,13 @@ mod reqwest_be { .await .context("error downloading file")?; - if !res.status().is_success() { - let code: u16 = res.status().into(); - return Err(anyhow!(DownloadError::HttpStatus(u32::from(code)))); + // If a download is being resumed, we expect a 206 response; + // otherwise, if the server ignored the range header, + // an error is thrown preemptively to avoid corruption. + let status = res.status().into(); + match (resume_from > 0, status) { + (true, 206) | (false, 200..=299) => {} + _ => return Err(DownloadError::HttpStatus(u32::from(status)).into()), } if let Some(len) = res.content_length() { From ca94cc70a6a040eb0db9e8f930811ef3f7f91eb6 Mon Sep 17 00:00:00 2001 From: Francisco Gouveia Date: Fri, 13 Feb 2026 22:18:27 +0000 Subject: [PATCH 3/3] test(downloads): check if an error is thrown if the server does not honor range --- src/download/tests.rs | 48 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/download/tests.rs b/src/download/tests.rs index 83eebd3198..36552a5ce2 100644 --- a/src/download/tests.rs +++ b/src/download/tests.rs @@ -57,7 +57,7 @@ mod curl { let target_path = tmpdir.path().join("downloaded"); write_file(&target_path, "123"); - let addr = serve_file(b"xxx45".to_vec()); + let addr = serve_file(b"xxx45".to_vec(), true); let from_url = format!("http://{addr}").parse().unwrap(); @@ -213,7 +213,7 @@ mod reqwest { let target_path = tmpdir.path().join("downloaded"); write_file(&target_path, "123"); - let addr = serve_file(b"xxx45".to_vec()); + let addr = serve_file(b"xxx45".to_vec(), true); let from_url = format!("http://{addr}").parse().unwrap(); @@ -258,6 +258,33 @@ mod reqwest { assert_eq!(std::fs::read_to_string(&target_path).unwrap(), "12345"); } + #[tokio::test] + async fn resume_partial_fails_if_server_ignores_range() { + let _guard = scrub_env().await; + let tmpdir = tmp_dir(); + let target_path = tmpdir.path().join("downloaded"); + write_file(&target_path, "123"); + + let addr = serve_file(b"xxx45".to_vec(), false); + let from_url = format!("http://{addr}").parse().unwrap(); + + Backend::Reqwest(TlsBackend::NativeTls) + .download_to_path( + &from_url, + &target_path, + true, + None, + Duration::from_secs(180), + ) + .await + .expect_err("download should fail if server ignores range"); + + assert!( + !target_path.exists(), + "partial file should have been deleted" + ); + } + #[tokio::test] async fn network_failure_does_not_delete_partial_file() { let _guard = scrub_env().await; @@ -299,11 +326,16 @@ pub fn write_file(path: &Path, contents: &str) { // A dead simple hyper server implementation. // For more info, see: // https://hyper.rs/guides/1/server/hello-world/ -async fn run_server(addr_tx: Sender, addr: SocketAddr, contents: Vec) { +async fn run_server( + addr_tx: Sender, + addr: SocketAddr, + contents: Vec, + honor_range: bool, +) { let svc = service_fn(move |req: Request| { let contents = contents.clone(); async move { - let res = serve_contents(req, contents); + let res = serve_contents(req, contents, honor_range); Ok::<_, Infallible>(res) } }); @@ -331,12 +363,12 @@ async fn run_server(addr_tx: Sender, addr: SocketAddr, contents: Vec } } -pub fn serve_file(contents: Vec) -> SocketAddr { +pub fn serve_file(contents: Vec, honor_range: bool) -> SocketAddr { let addr = ([127, 0, 0, 1], 0).into(); let (addr_tx, addr_rx) = channel(); thread::spawn(move || { - let server = run_server(addr_tx, addr, contents); + let server = run_server(addr_tx, addr, contents, honor_range); let rt = tokio::runtime::Runtime::new().expect("could not creating Runtime"); rt.block_on(server); }); @@ -348,9 +380,11 @@ pub fn serve_file(contents: Vec) -> SocketAddr { fn serve_contents( req: Request, contents: Vec, + honor_range: bool, ) -> hyper::Response> { let mut range_header = None; - let (status, body) = if let Some(range) = req.headers().get(hyper::header::RANGE) { + let (status, body) = if honor_range && let Some(range) = req.headers().get(hyper::header::RANGE) + { // extract range "bytes={start}-" let range = range.to_str().expect("unexpected Range header"); assert!(range.starts_with("bytes="));