@@ -88,22 +88,36 @@ pub async fn download_file(
8888 . timeout ( timeout)
8989 . header ( reqwest:: header:: RANGE , format ! ( "bytes={resume_from}-" ) )
9090 . send ( )
91- . await ?
92- . error_for_status ( ) ?;
93-
94- match classify_range_response ( & ranged, resume_from) {
95- RangeOutcome :: Resume { total } => ( ranged, Some ( total) , true ) ,
96- RangeOutcome :: Restart => {
97- let total = ranged. content_length ( ) ;
98- ( ranged, total, false )
99- }
100- RangeOutcome :: Reject => {
101- // Never consume an inconsistent partial response. Retry
102- // once without Range so the outer retry loop can progress.
103- drop ( ranged) ;
104- let plain = full_response ( client. get ( url) . timeout ( timeout) . send ( ) . await ?) ?;
105- let total = plain. content_length ( ) ;
106- ( plain, total, false )
91+ . await ?;
92+
93+ // A range-capable server answers `Range: bytes=<len>-` on an
94+ // already-complete file with `416 Range Not Satisfiable` (there's
95+ // nothing left to send). `error_for_status()` would surface that
96+ // as a hard failure and every retry would resend the same doomed
97+ // Range request against the same full-length file, permanently
98+ // failing instead of restarting. Treat it like an inconsistent
99+ // partial response: drop it and fall back to a plain full request.
100+ if ranged. status ( ) == reqwest:: StatusCode :: RANGE_NOT_SATISFIABLE {
101+ drop ( ranged) ;
102+ let plain = full_response ( client. get ( url) . timeout ( timeout) . send ( ) . await ?) ?;
103+ let total = plain. content_length ( ) ;
104+ ( plain, total, false )
105+ } else {
106+ let ranged = ranged. error_for_status ( ) ?;
107+ match classify_range_response ( & ranged, resume_from) {
108+ RangeOutcome :: Resume { total } => ( ranged, Some ( total) , true ) ,
109+ RangeOutcome :: Restart => {
110+ let total = ranged. content_length ( ) ;
111+ ( ranged, total, false )
112+ }
113+ RangeOutcome :: Reject => {
114+ // Never consume an inconsistent partial response. Retry
115+ // once without Range so the outer retry loop can progress.
116+ drop ( ranged) ;
117+ let plain = full_response ( client. get ( url) . timeout ( timeout) . send ( ) . await ?) ?;
118+ let total = plain. content_length ( ) ;
119+ ( plain, total, false )
120+ }
107121 }
108122 }
109123 } else {
@@ -600,6 +614,44 @@ mod tests {
600614 assert_eq ! ( tokio:: fs:: read( target. as_path( ) ) . await . unwrap( ) , full_body) ;
601615 }
602616
617+ // Regression for a retry after content-integrity failure: the file on
618+ // disk is already the *full* archive (a complete-but-corrupt previous
619+ // attempt), so the resume offset equals the file's total length. A
620+ // range-capable server answers that with `416 Range Not Satisfiable`
621+ // instead of `206`. Before the fix, `error_for_status()` turned that into
622+ // a hard error and every retry re-sent the same doomed Range request
623+ // against the same full-length file, so the download could never recover.
624+ #[ tokio:: test]
625+ async fn full_length_file_gets_416_and_restarts_with_a_plain_request ( ) {
626+ let server = MockServer :: start ( ) ;
627+ let dir = TempDir :: new ( ) . unwrap ( ) ;
628+ let target = temp_target ( & dir) ;
629+
630+ let full_body = b"the-full-archive-bytes-already-on-disk-from-a-prior-attempt" ;
631+ // Simulate a complete-but-corrupt archive already on disk (e.g. one
632+ // that failed hash verification and is being retried by the outer
633+ // content-integrity loop in `runtime.rs`).
634+ tokio:: fs:: write ( target. as_path ( ) , full_body) . await . unwrap ( ) ;
635+
636+ let range_mock = server. mock ( |when, then| {
637+ when. method ( GET )
638+ . path ( "/archive.bin" )
639+ . header ( "range" , format ! ( "bytes={}-" , full_body. len( ) ) ) ;
640+ then. status ( 416 ) ;
641+ } ) ;
642+ let plain_mock = server. mock ( |when, then| {
643+ when. method ( GET ) . path ( "/archive.bin" ) ;
644+ then. status ( 200 ) . header ( "content-length" , full_body. len ( ) . to_string ( ) ) . body ( full_body) ;
645+ } ) ;
646+
647+ let url = format ! ( "{}/archive.bin" , server. base_url( ) ) ;
648+ download_file ( & url, & target, "Downloading test" ) . await . unwrap ( ) ;
649+
650+ range_mock. assert_hits ( 1 ) ;
651+ plain_mock. assert_hits ( 1 ) ;
652+ assert_eq ! ( tokio:: fs:: read( target. as_path( ) ) . await . unwrap( ) , full_body) ;
653+ }
654+
603655 #[ tokio:: test]
604656 async fn mismatched_content_range_falls_back_to_a_plain_request ( ) {
605657 let server = MockServer :: start ( ) ;
0 commit comments