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

  1. Log the raw head response body to confirm size is truly absent versus a parse mismatch
  2. Compare responses from real GCS and any intermediary in the request path
  3. Retry once for transient anomalies
  4. 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

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.