{"record":{"id":"4f8a4a322fb34ff7","repo":"neondatabase/neon","slug":"missing-lastmodified-header","errorCode":null,"errorMessage":"Missing LastModified header","messagePattern":"Missing LastModified header","errorType":"exception","errorClass":"DownloadError","httpStatus":null,"severity":"error","filePath":"libs/remote_storage/src/s3_bucket.rs","lineNumber":323,"sourceCode":"                return Err(DownloadError::Other(\n                    anyhow::Error::new(e).context(\"download s3 object\"),\n                ));\n            }\n        };\n\n        // even if we would have no timeout left, continue anyways. the caller can decide to ignore\n        // the errors considering timeouts and cancellation.\n        let remaining = self.timeout.saturating_sub(started_at.elapsed());\n\n        let metadata = object_output.metadata().cloned().map(StorageMetadata);\n        let etag = object_output\n            .e_tag\n            .ok_or(DownloadError::Other(anyhow::anyhow!(\"Missing ETag header\")))?\n            .into();\n        let last_modified = object_output\n            .last_modified\n            .ok_or(DownloadError::Other(anyhow::anyhow!(\n                \"Missing LastModified header\"\n            )))?\n            .try_into()\n            .map_err(|e: ConversionError| DownloadError::Other(e.into()))?;\n\n        let body = object_output.body;\n        let body = ByteStreamAsStream::from(body);\n        let body = PermitCarrying::new(permit, body);\n        let body = TimedDownload::new(started_at, body);\n\n        let cancel_or_timeout = crate::support::cancel_or_timeout(remaining, cancel.clone());\n        let body = crate::support::DownloadStream::new(cancel_or_timeout, body);\n\n        Ok(Download {\n            metadata,\n            etag,\n            last_modified,\n            download_stream: Box::pin(body),\n        })","sourceCodeStart":305,"sourceCodeEnd":341,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/remote_storage/src/s3_bucket.rs#L305-L341","documentation":"Companion to the ETag check in S3Bucket's download path: after GetObject succeeds, LastModified is required to build the Download struct (it feeds Download.last_modified). If the response has no LastModified header, the download fails before any body bytes are read. Real S3 always sends it, so the cause is a non-compliant endpoint or header-stripping proxy.","triggerScenarios":"Calling GenericRemoteStorage::download / download_object against an S3-compatible emulator or gateway that omits the Last-Modified response header on GetObject.","commonSituations":"Mock S3 servers in CI that only implement body streaming; gateways that synthesize GetObject responses from metadata-less backends; misconfigured reverse proxies.","solutions":["Switch dev/CI to real S3 or MinIO, which always return Last-Modified","Verify with aws s3api get-object --query LastModified against the endpoint","Fix the emulator to emit a valid Last-Modified (RFC 7231 IMF-fixdate) on GetObject","Check intermediate proxies are not stripping standard headers"],"exampleFix":"// before: emulator GetObject handler omits headers\nresp.body(data);\n// after: set required metadata\nresp.insert_header(\"ETag\", format!(\"\\\"{}\\\"\", etag));\nresp.insert_header(\"Last-Modified\", httpdate::fmt_http_date(SystemTime::now()));\nresp.body(data);","handlingStrategy":"try-catch","validationCode":"use aws_sdk_s3::Client;\n\nasync fn endpoint_returns_last_modified(client: &Client, bucket: &str, key: &str) -> anyhow::Result<bool> {\n    let head = client.head_object().bucket(bucket).key(key).send().await?;\n    Ok(head.last_modified.is_some())\n}","typeGuard":"fn is_missing_last_modified(err: &remote_storage::DownloadError) -> bool {\n    matches!(err, remote_storage::DownloadError::Other(e)\n        if e.to_string().contains(\"Missing LastModified header\"))\n}","tryCatchPattern":"match storage.download(&path, &cancel).await {\n    Err(DownloadError::Other(e)) if e.to_string().contains(\"Missing LastModified header\") => {\n        // endpoint non-compliance: fail the fetch and surface endpoint config, no retry\n    }\n    other => other?,\n}","preventionTips":["Verify emulators emit Last-Modified before adding them to CI","Probe endpoints with aws s3api get-object --query LastModified","Keep proxies from stripping standard response headers"],"tags":["s3","rust","aws-sdk","remote-storage","http-header"],"backgroundTag":"missing-http-response-header","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}