{"record":{"id":"27314601acd40f5f","repo":"neondatabase/neon","slug":"received-listversions-response-for-key-key-with-273146","errorCode":null,"errorMessage":"Received ListVersions response for key={key} with version_id='null', indicating either disabled versioning, or legacy objects with null version id values","messagePattern":"Received ListVersions response for key=(.+?) with version_id='null', indicating either disabled versioning, or legacy objects with null version id values","errorType":"exception","errorClass":"TimeTravelError","httpStatus":null,"severity":"error","filePath":"libs/remote_storage/src/gcs_bucket.rs","lineNumber":1232,"sourceCode":"       tracing::info!(\n           \"Built list for time travel with {} versions and deletions\",\n           versions_and_deletes.len()\n       );\n\n       // Work on the list of references instead of the objects directly,\n       // otherwise we get lifetime errors in the sort_by_key call below.\n       let mut versions_and_deletes = versions_and_deletes.iter().collect::<Vec<_>>();\n\n       versions_and_deletes.sort_by_key(|vd| (&vd.key, &vd.last_modified));\n\n       let mut vds_for_key = HashMap::<_, Vec<_>>::new();\n\n       for vd in &versions_and_deletes {\n           let GCSVersion { key, .. } = &vd;\n           if Some(vd.id.0.as_str()) == Some(\"null\") {\n               // TODO: check the behavior of using the SDK on a non-versioned container\n               return Err(TimeTravelError::Other(anyhow::anyhow!(\n                   \"Received ListVersions response for key={key} with version_id='null', \\\n                   indicating either disabled versioning, or legacy objects with null version id values\"\n               )));\n           }\n           tracing::trace!(\"Parsing version key={key} id={:?}\", vd.id);\n           vds_for_key.entry(key).or_default().push(vd);\n       }\n\n       let warn_threshold = 3;\n       let max_retries = 10;\n       let is_permanent = |e: &_| matches!(e, TimeTravelError::Cancelled);\n\n       for (key, versions) in vds_for_key {\n           let last_vd = versions.last().unwrap();\n           let key = self.relative_path_to_gcs_object(key);\n           if last_vd.last_modified > done_if_after {\n               /// Case 1: we have a recent object outside of our restore window.\n               tracing::trace!(\"Key {key} has version later than done_if_after, skipping\");\n               continue;","sourceCodeStart":1214,"sourceCodeEnd":1250,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/remote_storage/src/gcs_bucket.rs#L1214-L1250","documentation":"GCS port of the same time-travel guard as the Azure backend: while grouping listed versions per key, a version whose id is the literal string \\\"null\\\" aborts the listing. GCS identifies versions by numeric generations, so a \\\"null\\\" id signals a non-versioned/legacy situation — data imported with S3-style 'null' version markers or synthetic ids from migration tooling — which time travel cannot address.","triggerScenarios":"list_versions / time travel on a GCS bucket containing objects whose generation was recorded as the string \\\"null\\\" — typically data migrated from S3-style stores or written by tooling that emits 'null' version ids for non-versioned objects.","commonSituations":"Buckets populated by S3-to-GCS migration tools; test fixtures with hand-written version ids; time travel pointed at data written outside the normal GCS write path.","solutions":["Identify the offending keys (the error names the key) and rewrite those objects through normal GCS writes so they carry real generations","Re-run the migration/import step that produced 'null' version ids, dropping the null markers","Verify listing tooling maps GCS generations directly (they are numeric strings) instead of S3 null-version semantics","Route non-versioned legacy paths away from time-travel APIs"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before time travel, scan one page of versions for 'null' ids and fail with guidance.\nasync fn assert_gcs_generations(storage: &Arc<GenericRemoteStorage>, key: &RemotePath, cancel: &CancellationToken) -> anyhow::Result<()> {\n    let versions = storage.list_versions(key, Some(MaxKeys::new(100)), cancel).await\n        .map_err(|e| anyhow::anyhow!(\"listing failed: {e:#}\"))?;\n    let null_ids = versions.versions.iter().filter(|v| v.id.0 == \"null\").count();\n    anyhow::ensure!(null_ids == 0,\n        \"{null_ids} versions of {key} carry a 'null' id (migrated/legacy data); rewrite them via normal GCS writes\");\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":"// Detect the family and report the remediation.\nlet versions = match storage.list_versions(key, None, &cancel).await {\n    Ok(v) => v,\n    Err(TimeTravelError::Other(e)) if format!(\"{e:#}\").contains(\"version_id='null'\") => {\n        anyhow::bail!(\"'null' version id found (legacy/migrated data without generations); rewrite affected objects before time travel\");\n    }\n    Err(e) => return Err(e.into()),\n};","preventionTips":["Migrate S3-style data into GCS without carrying 'null' version markers — map generations directly","Validate imported datasets with a listing sweep before enabling time travel on them","Write all versioned data through the normal GCS write path so generations are always assigned"],"tags":["gcs","google-cloud-storage","versioning","time-travel","data-migration"],"backgroundTag":"versioning-disabled","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}