fix: accept GCS gzip responses without Content-Length - #782
Conversation
GCS serves large objects stored with `Content-Encoding: gzip` using chunked transfer with no `Content-Length` (and decompressive transcoding when the client does not accept gzip encoding). The GET path required `Content-Length` unconditionally and failed with `MissingContentLength`, even though a chunked body is a valid self-delimiting response (RFC 9112 §6.2 forbids `Content-Length` with `Transfer-Encoding: chunked`). Add `HeaderConfig::stored_size_header`: when `Content-Length` is absent the size falls back to this header. GCS sets it to `x-goog-stored-content-length` (always present); S3, Azure and HTTP leave it `None`, so a missing `Content-Length` remains an error for them. This fixes the reported `MissingContentLength` failure. Some transcoded GCS responses also omit the ETag and still fail with `MissingEtag`; that is left for a follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alamb
left a comment
There was a problem hiding this comment.
Thanks @nkemnitz -- this seems ok to me, but I am not a GCS expert
Can you please provide some documentation / evidence that GCS behaves the way that this PR's description claims? Ideally we can include those links in the docs for future readers who find the behavior?
| Err(e) => return Err(e), | ||
| }; | ||
|
|
||
| // Prefer `Content-Length`, falling back to a store-provided size header: GCS omits |
There was a problem hiding this comment.
is there documentation on this behavior you can link to so we can verify this?
There was a problem hiding this comment.
The description for what x-goog-stored-content-length contains: https://docs.cloud.google.com/storage/docs/xml-api/reference-headers#xgoogstoredcontentlength
In the Google Cloud Rust repo, a code maintainer reports that this header is indeed always present: googleapis/google-cloud-rust#3713 (reply in thread)
The part that confirms that Content-Length and Content-Encoding get dropped for decompressive transcoding: https://docs.cloud.google.com/storage/docs/transcoding#decompressive_transcoding
(Although I don't really care about decompressive transcoding - it creates large transfers and as described under the same link in the paragraph below, it prevents integrity checks)
I could not find any official documentation regarding the chunked Transfer-Encoding switch... only can see that it does... Could weaken that claim to "Google may omit the Content-Length".
Other finds:
- google-cloud-cpp prefers
x-goog-stored-content-length>content-range>content-length - google-cloud-rust only uses x-goog-stored-content-length and nothing else?
- Their python code seems to use it to distinguish truncated downloads from transcoded ones
Which issue does this PR close?
Part of #774 (does not fully close it — see below).
Rationale for this change
GCS serves large objects stored with
Content-Encoding: gzipusing chunked transfer with noContent-Length(and decompressive transcoding when the client does not accept gzip encoding).ObjectStore::get/headon GCS requiredContent-Lengthunconditionally and failed withGeneric { store: "GCS", source: Header { source: MissingContentLength } }, even though a chunked, self-delimiting body is a valid response (RFC 9112 §6.2 forbidsContent-LengthalongsideTransfer-Encoding: chunked).What changes are included in this PR?
HeaderConfiggainsstored_size_header: Option<&'static str>. WhenContent-Lengthis absent,header_metareads the object size from this header. GCS sets it tox-goog-stored-content-length(always present); S3, Azure and theHTTP store leave it
None, so a missingContent-Lengthstays a hard error for them.Are there any user-facing changes?
get()/head()now succeed on chunked gzip GCS objects. On a server-decompressed (transcoded) read,ObjectMeta.sizeis the stored (compressed) size, since the decompressed length is not known without reading the body; on a passthrough read (Accept-Encoding: gzip) it is exact.Not fully resolved: some transcoded GCS responses (default reads without
Accept-Encoding: gzip) also omit the ETag entirely and still fail withMissingEtag. Left for a follow-up.🤖 AI disclaimer:
All the code written by Claude. I made the changes as targeted and minimal as possible, for now only focusing on the chunked encoding, because that's my major blocker. Decompressive transcoding feels kind of niche. And the ETag handling involves some more thought and knowledge about this repo. E.g. I think the different Cloud vendors rely on custom metadata
versionheaders to allow resuming downloads, rather than the ETag(?)...