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
- Retry the listing — transient malformed responses are rare but possible
- Capture the raw JSON body of the list response and confirm updated is truly absent versus a deserialization mismatch in GCSObject
- Remove or fix any GCS-compatible intermediary so responses come from the real JSON API
- 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
- Avoid GCS-compatible proxies/emulators in production paths; talk to the real JSON API
- Log raw list responses (trace level) in environments where anomalies recur
- Treat single-field-missing errors as data-quality signals: capture and report, don't silently drop the version
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
- Missing size (content length) header
- no items returned
- max keys reached
- Missing ETag header
- Received ListVersions response for key={key} with version_id
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/391979a84d4001e5.
Report an issue: GitHub.