@@ -244,7 +244,48 @@ impl<T: GetClient> GetContext<T> {
244244 return Err ( Self :: err ( e) ) ;
245245 }
246246
247- body = retry_body;
247+ // Validate the Content-Range of the retry response
248+ let content_range =
249+ parse_range ( & parts. headers ) . map_err ( Self :: err) ?;
250+ let actual = content_range. range ;
251+
252+ // Exact match — use body as-is
253+ if actual == range {
254+ body = retry_body;
255+ } else if actual. start <= range. start && actual. end >= range. end
256+ {
257+ // Received range is a superset for requested content,
258+ // skip leading bytes to align to the needed offset.
259+ let skip = ( range. start - actual. start ) as usize ;
260+ let mut skipped = 0 ;
261+ let mut retry_body = retry_body;
262+ while skipped < skip {
263+ let frame = retry_body. frame ( ) . await
264+ . ok_or_else ( || Self :: err ( GetResultError :: UnexpectedRange {
265+ expected : range. clone ( ) , actual : actual. clone ( ) ,
266+ } ) ) ?
267+ . map_err ( Self :: err) ?;
268+ let Some ( bytes) = frame. into_data ( ) . ok ( ) else { continue } ;
269+ let remaining = skip - skipped;
270+ if bytes. len ( ) <= remaining {
271+ skipped += bytes. len ( ) ;
272+ } else {
273+ let keep = bytes. slice ( remaining..) ;
274+ range. start += keep. len ( ) as u64 ;
275+ body = retry_body;
276+ let etag = Some ( etag. clone ( ) ) ;
277+ return Ok ( Some ( ( keep, ( ctx, body, etag, range) ) ) ) ;
278+ }
279+ }
280+ body = retry_body;
281+ } else {
282+ return Err ( Self :: err (
283+ GetResultError :: UnexpectedRange {
284+ expected : range,
285+ actual,
286+ } ,
287+ ) ) ;
288+ }
248289 }
249290 ( Err ( e) , _) => return Err ( Self :: err ( e) ) ,
250291 }
@@ -772,4 +813,57 @@ mod http_tests {
772813 "Generic HTTP error: HTTP error: request or response body error"
773814 ) ;
774815 }
816+
817+ #[ tokio:: test]
818+ async fn test_retry_validate_content_range ( ) {
819+ let mock = MockServer :: new ( ) . await ;
820+ let retry = RetryConfig {
821+ backoff : Default :: default ( ) ,
822+ max_retries : 3 ,
823+ retry_timeout : Duration :: from_secs ( 1000 ) ,
824+ } ;
825+
826+ let options = ClientOptions :: new ( ) . with_allow_http ( true ) ;
827+ let store = HttpBuilder :: new ( )
828+ . with_client_options ( options)
829+ . with_retry ( retry)
830+ . with_url ( mock. url ( ) )
831+ . build ( )
832+ . unwrap ( ) ;
833+
834+ let path = Path :: from ( "test" ) ;
835+
836+ mock. push (
837+ Response :: builder ( )
838+ . header ( CONTENT_LENGTH , 10 )
839+ . header ( ETAG , "abc" )
840+ . body ( Chunked :: new ( vec ! [
841+ Ok ( Bytes :: from_static( b"hello" ) ) ,
842+ Err ( ( ) ) ,
843+ ] ) )
844+ . unwrap ( ) ,
845+ ) ;
846+
847+ mock. push_fn ( |req| {
848+ assert_eq ! (
849+ req. headers( ) . get( RANGE ) . unwrap( ) . to_str( ) . unwrap( ) ,
850+ "bytes=5-9"
851+ ) ;
852+
853+ Response :: builder ( )
854+ . status ( StatusCode :: PARTIAL_CONTENT )
855+ . header ( CONTENT_LENGTH , 10 )
856+ . header ( ETAG , "abc" )
857+ . header ( CONTENT_RANGE , "bytes 0-9/10" )
858+ . body ( "helloworld" . to_string ( ) )
859+ . unwrap ( )
860+ } ) ;
861+
862+ let result = store. get ( & path) . await . unwrap ( ) . bytes ( ) . await . unwrap ( ) ;
863+ assert_eq ! (
864+ result. as_ref( ) ,
865+ b"helloworld" ,
866+ "expected correct 10-byte content"
867+ ) ;
868+ }
775869}
0 commit comments