Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion src/client/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,48 @@ impl<T: GetClient> GetContext<T> {
return Err(Self::err(e));
}

body = retry_body;
// Validate the Content-Range of the retry response
let content_range =
parse_range(&parts.headers).map_err(Self::err)?;
let actual = content_range.range;

// Exact match — use body as-is
if actual == range {
body = retry_body;
} else if actual.start <= range.start && actual.end >= range.end
{
// Received range is a superset for requested content,
// skip leading bytes to align to the needed offset.
let skip = (range.start - actual.start) as usize;
let mut skipped = 0;
let mut retry_body = retry_body;
while skipped < skip {
let frame = retry_body.frame().await
.ok_or_else(|| Self::err(GetResultError::UnexpectedRange {
expected: range.clone(), actual: actual.clone(),
}))?
.map_err(Self::err)?;
let Some(bytes) = frame.into_data().ok() else { continue };
let remaining = skip - skipped;
if bytes.len() <= remaining {
skipped += bytes.len();
} else {
let keep = bytes.slice(remaining..);
range.start += keep.len() as u64;
body = retry_body;
let etag = Some(etag.clone());
return Ok(Some((keep, (ctx, body, etag, range))));
}
}
body = retry_body;
} else {
return Err(Self::err(
GetResultError::UnexpectedRange {
expected: range,
actual,
},
));
}
}
(Err(e), _) => return Err(Self::err(e)),
}
Expand Down Expand Up @@ -772,4 +813,57 @@ mod http_tests {
"Generic HTTP error: HTTP error: request or response body error"
);
}

#[tokio::test]
async fn test_retry_validate_content_range() {
let mock = MockServer::new().await;
let retry = RetryConfig {
backoff: Default::default(),
max_retries: 3,
retry_timeout: Duration::from_secs(1000),
};

let options = ClientOptions::new().with_allow_http(true);
let store = HttpBuilder::new()
.with_client_options(options)
.with_retry(retry)
.with_url(mock.url())
.build()
.unwrap();

let path = Path::from("test");

mock.push(
Response::builder()
.header(CONTENT_LENGTH, 10)
.header(ETAG, "abc")
.body(Chunked::new(vec![
Ok(Bytes::from_static(b"hello")),
Err(()),
]))
.unwrap(),
);

mock.push_fn(|req| {
assert_eq!(
req.headers().get(RANGE).unwrap().to_str().unwrap(),
"bytes=5-9"
);

Response::builder()
.status(StatusCode::PARTIAL_CONTENT)
.header(CONTENT_LENGTH, 10)
.header(ETAG, "abc")
.header(CONTENT_RANGE, "bytes 0-9/10")
.body("helloworld".to_string())
.unwrap()
});

let result = store.get(&path).await.unwrap().bytes().await.unwrap();
assert_eq!(
result.as_ref(),
b"helloworld",
"expected correct 10-byte content"
);
}
}