neondatabase/neon · error · DownloadError

no 'updated' field

Error message

no 'updated' field

What it means

Every object in the GCS version listing must carry an updated (last-modified, RFC 3339) timestamp because time travel relies on it for ordering; the code deliberately does not filter_map so that a None aborts the listing. GCS normally always returns updated, so a None indicates a malformed or unexpected API response rather than normal operation.

Source

Thrown at libs/remote_storage/src/gcs_bucket.rs:285

                
            let res = response.json::<GCSListResponse>()
                .await
                .map_err(|e| DownloadError::Other(e.into()))?;
                    
            // fill up our results vec, 
            continuation_token = res.next_page_token;
            
            let version_listing = 
                res.items
                    .ok_or_else(|| DownloadError::Other(anyhow::anyhow!("no items returned")))?
                    .into_iter()
                    .map(| GCSObject { name, updated, time_deleted, generation, .. } | {
                        // don't `filter_map`, a `None` for `last_modified` ('updated') is bad for
                        // time travel, so catch it.
                        if updated.is_none() {
                           return Err(
                               DownloadError::Other(
                                   anyhow::anyhow!("no 'updated' field")
                               )
                           )
                        }
                        Ok(
                            GCSVersion {
                                key: self.gcs_object_to_relative_path(&name),
                                last_modified: to_system_time(updated).unwrap(),
                                id: VersionId(generation.expect("no version id")),
                                time_deleted: to_system_time(time_deleted),
                            }
                        )
                    }).collect::<Result<Vec<GCSVersion>, _>>();
                
            versions.versions.extend(version_listing?);

            if let Some(max_keys) = max_keys {
                if versions.versions.len() >= max_keys.get().try_into().unwrap() {
                    return Err(DownloadError::Other(

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Retry the listing — transient malformed responses are rare but possible
  2. Capture the raw JSON body of the list response and confirm updated is truly absent versus a deserialization mismatch in GCSObject
  3. Remove or fix any GCS-compatible intermediary so responses come from the real JSON API
  4. If reproducible on one object, inspect that object's metadata directly (gsutil stat / objects.get) and report upstream
Defensive patterns

Strategy: try-catch

Try / catch

// Rare, anomalous-response error: retry once, then surface with the key for diagnosis.
let versions = match storage.list_versions(key, None, &cancel).await {
    Ok(v) => v,
    Err(DownloadError::Other(e)) if format!("{e:#}").contains("no 'updated' field") => {
        tracing::warn!("malformed listing response for {key}, retrying once");
        tokio::time::sleep(Duration::from_millis(500)).await;
        storage.list_versions(key, None, &cancel).await?
    }
    Err(e) => return Err(e),
};

Prevention

When it happens

Trigger: list_versions where any listed object version lacks the updated JSON field — an API shape change, a partially-initialized object record, or a GCS-compatible gateway/emulator that drops the field.

Common situations: S3-compatible proxies or emulators fronting GCS that omit fields; GCP API behavior drift; extremely rare metadata anomalies on freshly created objects.

Related errors


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/391979a84d4001e5. Report an issue: GitHub.