Describe the bug
ObjectStore::get on the GCS backend fails with Error::Generic { store: "GCS", source: Header { source: MissingContentLength } } for any object stored with Content-Encoding: gzip. GCS serves these via Transfer-Encoding: chunked with no Content-Length, and object_store treats a missing Content-Length as fatal.
It manifests in two ways, and crucially a client cannot fully avoid it:
- Default reads (no
Accept-Encoding): GCS applies decompressive transcoding — it decompresses the object server-side and streams the result chunked, with no Content-Length. Every gzip object fails, at any size.
- Even with
Accept-Encoding: gzip (which returns the raw stored bytes): objects whose stored size exceeds ~8 MiB are still served chunked with no Content-Length (empirically the cutover is between 9 and 10 MB), so they fail too. Only small objects read with Accept-Encoding: gzip succeed.
So whether you receive the transcoded (uncompressed) bytes or the raw (compressed) bytes, the response is chunked with no Content-Length — and object_store rejects it before returning any data. head() and range reads on gzip objects fail as well.
These are valid HTTP/1.1 responses: a chunked body is self-delimiting, and RFC 9112 §6.2 states a sender MUST NOT send Content-Length together with Transfer-Encoding;
RFC 9112 §6.3 says the chunked framing determines the body length and overrides any Content-Length. object_store is therefore requiring a header the spec forbids on exactly these responses.
To Reproduce
Observe the offending response with no credentials against Google's public demo data (the ?cb= cache-buster forces origin past the public edge cache):
$ curl -sD - -o /dev/null \
"https://storage.googleapis.com/neuroglancer-public-data/kasthuri2011/ground_truth/6_6_30/5376-5440_6656-6720_896-960?cb=$RANDOM" \
| grep -iE "HTTP/|content-length|transfer-encoding|x-goog-stored-content-encoding"
HTTP/2 200
transfer-encoding: chunked
x-goog-stored-content-encoding: gzip
# (no content-length)
object_store then fails on that same public object (authenticated read — a service account or on GCP; verified with object_store 0.13.1 and main de0029a):
use object_store::gcp::GoogleCloudStorageBuilder;
use object_store::{path::Path, ObjectStore, ObjectStoreExt};
let store = GoogleCloudStorageBuilder::new()
.with_bucket_name("neuroglancer-public-data")
.with_service_account_path(sa) // or workload identity on GCP
.build()?;
store
.get(&Path::from(
"kasthuri2011/ground_truth/6_6_30/5376-5440_6656-6720_896-960",
))
.await?; // Err: Generic { store: "GCS", source: Header { source: MissingContentLength } }
For a self-contained object of any size in your own bucket (a >8 MiB one fails even with Accept-Encoding: gzip):
head -c 20000000 /dev/urandom | gzip | gsutil -h "Content-Encoding:gzip" cp - gs://YOUR_BUCKET/big.gz
Note: the bundled fake-gcs-server does not emulate decompressive transcoding (it always
returns a Content-Length), so it cannot reproduce this. The crate's own MockServer can — push a
response with a chunked body and no Content-Length header.
Expected behavior
A full GET of a valid, self-delimiting (chunked / HTTP-2) response should succeed by reading the body to completion rather than requiring a Content-Length header that the HTTP spec forbids on chunked responses.
Additional context
- Affects gzip only;
br/zstd/uncompressed objects are served identity (with Content-Length) and read fine.
- Root cause:
header_meta requires CONTENT_LENGTH unconditionally (header.rs#L144, in header_meta L114), and the GET path derives ObjectMeta.size/range from it before streaming
(get.rs#L314 → #L333).
- Wire evidence (>8 MiB gzip object, authenticated,
Accept-Encoding: gzip):
Transfer-Encoding: chunked, no Content-Length, x-goog-stored-content-encoding: gzip,
x-goog-stored-content-length: <n>.
Prior art — every other major GCS client reads to EOF (commit-pinned)
AI disclaimer:
- 🤷♂️ ran into the bug
- 🤖 did initial investigation
- 🤷♂️ verified investigation results (checked RFC, response headers, and
google-cloud-storage behavior)
- 🤖 helped writing the report
- 🤷♂️ proofread and made funny AI disclaimer list
Describe the bug
ObjectStore::geton the GCS backend fails withError::Generic { store: "GCS", source: Header { source: MissingContentLength } }for any object stored withContent-Encoding: gzip. GCS serves these viaTransfer-Encoding: chunkedwith noContent-Length, and object_store treats a missingContent-Lengthas fatal.It manifests in two ways, and crucially a client cannot fully avoid it:
Accept-Encoding): GCS applies decompressive transcoding — it decompresses the object server-side and streams the result chunked, with noContent-Length. Every gzip object fails, at any size.Accept-Encoding: gzip(which returns the raw stored bytes): objects whose stored size exceeds ~8 MiB are still served chunked with noContent-Length(empirically the cutover is between 9 and 10 MB), so they fail too. Only small objects read withAccept-Encoding: gzipsucceed.So whether you receive the transcoded (uncompressed) bytes or the raw (compressed) bytes, the response is chunked with no
Content-Length— and object_store rejects it before returning any data.head()and range reads on gzip objects fail as well.These are valid HTTP/1.1 responses: a chunked body is self-delimiting, and RFC 9112 §6.2 states a sender MUST NOT send
Content-Lengthtogether withTransfer-Encoding;RFC 9112 §6.3 says the chunked framing determines the body length and overrides any
Content-Length. object_store is therefore requiring a header the spec forbids on exactly these responses.To Reproduce
Observe the offending response with no credentials against Google's public demo data (the
?cb=cache-buster forces origin past the public edge cache):object_store then fails on that same public object (authenticated read — a service account or on GCP; verified with object_store 0.13.1 and main
de0029a):For a self-contained object of any size in your own bucket (a >8 MiB one fails even with
Accept-Encoding: gzip):head -c 20000000 /dev/urandom | gzip | gsutil -h "Content-Encoding:gzip" cp - gs://YOUR_BUCKET/big.gzExpected behavior
A full GET of a valid, self-delimiting (chunked / HTTP-2) response should succeed by reading the body to completion rather than requiring a
Content-Lengthheader that the HTTP spec forbids on chunked responses.Additional context
br/zstd/uncompressed objects are served identity (withContent-Length) and read fine.header_metarequiresCONTENT_LENGTHunconditionally (header.rs#L144, inheader_metaL114), and the GET path derivesObjectMeta.size/rangefrom it before streaming(
get.rs#L314→#L333).Accept-Encoding: gzip):Transfer-Encoding: chunked, noContent-Length,x-goog-stored-content-encoding: gzip,x-goog-stored-content-length: <n>.Prior art — every other major GCS client reads to EOF (commit-pinned)
response.iter_content();x-goog-stored-content-lengthonly a retry heuristicReader.Remain()returns-1for chunked /Decompressed; CRC skipped on transcodingHasUnreadData(); falls back tox-goog-stored-content-lengthfor sizeres.Body, reads to EOFAI disclaimer:
google-cloud-storagebehavior)