neondatabase/neon · error · DownloadError
Missing size (content length) header
Error message
Missing size (content length) header
What it means
Building a ListingObject from a GCS object-metadata response requires the object's size; the parsed JSON had no size field, so listing construction fails. GCS always reports size for existing objects, so absence indicates an anomalous response (a gateway/emulator dropping the field, or an API shape change) rather than normal operation.
Source
Thrown at libs/remote_storage/src/gcs_bucket.rs:1364
Ok(())
}
async fn head_object(
&self,
key: &RemotePath,
cancel: &CancellationToken,
) -> Result<ListingObject, DownloadError> {
let path = self
.relative_path_to_gcs_object(key)
.trim_start_matches("/")
.to_string();
let resp = self.head_object(path.clone(), cancel).await?;
let last_modified: SystemTime = to_system_time(resp.updated).unwrap_or(SystemTime::now());
let Some(size) = resp.size else {
return Err(DownloadError::Other(anyhow::anyhow!(
"Missing size (content length) header"
)));
};
Ok(ListingObject {
key: self.gcs_object_to_relative_path(&path),
last_modified,
size: size as u64,
})
}
async fn list_versions(
&self,
prefix: Option<&RemotePath>,
mode: ListingMode,
max_keys: Option<NonZeroU32>,
cancel: &CancellationToken,
) -> Result<crate::VersionListing, DownloadError> {
let kind = RequestKind::ListVersions;View on GitHub (pinned to 8f60b04da4)
Solutions
- Log the raw head response body to confirm size is truly absent versus a parse mismatch
- Compare responses from real GCS and any intermediary in the request path
- Retry once for transient anomalies
- Fix the intermediary's field mapping or bypass it
Example fix
// before: absent size aborts listing construction
let Some(size) = resp.size else {
return Err(DownloadError::Other(anyhow::anyhow!("Missing size (content length) header")));
};
// after: fall back to a separate stat when the head body lacks size
let size = match resp.size {
Some(s) => s,
None => self.stat_object(&key, cancel).await?.size,
}; Defensive patterns
Strategy: try-catch
Try / catch
// Anomalous metadata: isolate the failing key, keep the rest of the listing.
let objects: Vec<ListingObject> = futures::stream::iter(keys)
.filter_map(|key| async move {
match storage.list_objects_from_head(key.clone(), &cancel).await {
Ok(obj) => Some(obj),
Err(DownloadError::Other(e)) if format!("{e:#}").contains("Missing size") => {
tracing::error!("metadata for {key} lacks size; skipping: {e:#}");
None
}
Err(e) => { tracing::error!("head failed for {key}: {e:#}"); None }
}
})
.collect()
.await; Prevention
- Regression-test listing code against the exact gateway/emulator used in production
- Log raw metadata bodies (debug) when integrating a new storage endpoint
- Treat single-missing-field errors as canaries for response-shape drift, investigate the intermediary
When it happens
Trigger: list_objects_from_head via head_object succeeding HTTP-wise but returning a JSON body without size — typically a GCS-compatible intermediary, or an error body that parsed as an object.
Common situations: S3-compatible fronts for GCS dropping size in metadata; emulators in local dev; rare malformed responses.
Related errors
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/f3e7955dcc2fef85.
Report an issue: GitHub.