Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions src/aws/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,7 @@ impl GetClient for S3Client {
const HEADER_CONFIG: HeaderConfig = HeaderConfig {
etag_required: false,
last_modified_required: false,
stored_size_header: None,
version_header: Some(VERSION_HEADER),
user_defined_metadata_prefix: Some(USER_DEFINED_METADATA_HEADER_PREFIX),
};
Expand Down
1 change: 1 addition & 0 deletions src/azure/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,7 @@ impl GetClient for AzureClient {
const HEADER_CONFIG: HeaderConfig = HeaderConfig {
etag_required: true,
last_modified_required: true,
stored_size_header: None,
version_header: Some(VERSION_HEADER),
user_defined_metadata_prefix: Some(USER_DEFINED_METADATA_HEADER_PREFIX),
};
Expand Down
49 changes: 49 additions & 0 deletions src/client/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ mod tests {
const CFG: HeaderConfig = HeaderConfig {
etag_required: false,
last_modified_required: false,
stored_size_header: None,
version_header: None,
user_defined_metadata_prefix: Some("x-test-meta-"),
};
Expand Down Expand Up @@ -507,6 +508,54 @@ mod tests {
assert_eq!(err.to_string(), "Requested 2..6, got 2..4");
}

#[test]
fn test_get_missing_content_length() {
// Mirrors the GCS config: size falls back to x-goog-stored-content-length.
const RELAXED: HeaderConfig = HeaderConfig {
stored_size_header: Some("x-goog-stored-content-length"),
..CFG
};
let path = Path::from("test");

let resp = |headers: &[(&str, &str)]| {
let mut builder = http::Response::builder().status(StatusCode::OK);
for (k, v) in headers {
builder = builder.header(*k, *v);
}
builder.body(()).unwrap().into_parts().0
};

// No Content-Length, stored-size header present -> best-effort size from fallback.
let r = resp(&[("x-goog-stored-content-length", "355")]);
let (range, meta) = get_range_meta(RELAXED, &path, None, &r).unwrap();
assert_eq!(meta.size, 355);
assert_eq!(range, 0..355);

// No Content-Length and the stored-size header also absent -> still an error.
let r = resp(&[]);
let err = get_range_meta(RELAXED, &path, None, &r).unwrap_err();
assert_eq!(
err.to_string(),
"Content-Length Header missing from response"
);

// A present Content-Length always wins over the fallback.
let r = resp(&[
("content-length", "10"),
("x-goog-stored-content-length", "355"),
]);
let (_, meta) = get_range_meta(RELAXED, &path, None, &r).unwrap();
assert_eq!(meta.size, 10);

// With the strict default (S3/Azure), a missing Content-Length is still fatal.
let r = resp(&[]);
let err = get_range_meta(CFG, &path, None, &r).unwrap_err();
assert_eq!(
err.to_string(),
"Content-Length Header missing from response"
);
}

#[test]
fn test_get_attributes() {
let resp = make_response(
Expand Down
12 changes: 12 additions & 0 deletions src/client/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ pub(crate) struct HeaderConfig {
/// Defaults to `true`
pub last_modified_required: bool,

/// Header to read the object size from when `Content-Length` is absent.
///
/// GCS omits `Content-Length` on chunked `Content-Encoding: gzip` responses — large
/// bodies, or decompressive transcoding — but always sends `x-goog-stored-content-length`.
/// Stores that always send a `Content-Length` (S3, Azure) leave this `None`, so a missing
/// `Content-Length` stays an error for them.
pub stored_size_header: Option<&'static str>,

/// The version header name if any
pub version_header: Option<&'static str>,

Expand Down Expand Up @@ -139,8 +147,12 @@ pub(crate) fn header_meta(
Err(e) => return Err(e),
};

// Prefer `Content-Length`, falling back to a store-provided size header: GCS omits
// `Content-Length` on chunked gzip responses (large bodies, or transcoding) but always
// sends `x-goog-stored-content-length`. Stores without such a header still require it.
let content_length = headers
.get(CONTENT_LENGTH)
.or_else(|| cfg.stored_size_header.and_then(|h| headers.get(h)))
.ok_or(Error::MissingContentLength)?;

let content_length = content_length
Expand Down
4 changes: 4 additions & 0 deletions src/gcp/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const VERSION_HEADER: &str = "x-goog-generation";
const DEFAULT_CONTENT_TYPE: &str = "application/octet-stream";
const USER_DEFINED_METADATA_HEADER_PREFIX: &str = "x-goog-meta-";
const STORAGE_CLASS: &str = "x-goog-storage-class";
const STORED_CONTENT_LENGTH_HEADER: &str = "x-goog-stored-content-length";

static VERSION_MATCH: HeaderName = HeaderName::from_static("x-goog-if-generation-match");

Expand Down Expand Up @@ -617,9 +618,12 @@ impl GoogleCloudStorageClient {
#[async_trait]
impl GetClient for GoogleCloudStorageClient {
const STORE: &'static str = STORE;
// GCS omits Content-Length on chunked gzip responses (large bodies, or decompressive
// transcoding); the size is recovered from x-goog-stored-content-length instead.
const HEADER_CONFIG: HeaderConfig = HeaderConfig {
etag_required: true,
last_modified_required: true,
stored_size_header: Some(STORED_CONTENT_LENGTH_HEADER),
version_header: Some(VERSION_HEADER),
user_defined_metadata_prefix: Some(USER_DEFINED_METADATA_HEADER_PREFIX),
};
Expand Down
1 change: 1 addition & 0 deletions src/http/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ impl GetClient for Client {
const HEADER_CONFIG: HeaderConfig = HeaderConfig {
etag_required: false,
last_modified_required: false,
stored_size_header: None,
version_header: None,
user_defined_metadata_prefix: None,
};
Expand Down