neondatabase/neon · error · DownloadError
can't convert time '{last_modified}': {e}
Error message
can't convert time '{last_modified}': {e} What it means
After a successful HeadObject, last_modified (an aws_smithy_types::DateTime) is converted with SystemTime::try_from; the conversion fails when the timestamp is outside SystemTime's range, most commonly a date before the Unix epoch. The offending timestamp is embedded in the message, so you can see exactly what the endpoint reported.
Source
Thrown at libs/remote_storage/src/s3_bucket.rs:829
AttemptOutcome::Err,
started_at,
);
return Err(DownloadError::Other(
anyhow::Error::new(e).context("s3 head object"),
));
}
};
let (Some(last_modified), Some(size)) = (data.last_modified, data.content_length) else {
return Err(DownloadError::Other(anyhow!(
"head_object doesn't contain last_modified or content_length"
)))?;
};
Ok(ListingObject {
key: key.to_owned(),
last_modified: SystemTime::try_from(last_modified).map_err(|e| {
DownloadError::Other(anyhow!("can't convert time '{last_modified}': {e}"))
})?,
size: size as u64,
})
}
async fn upload(
&self,
from: impl Stream<Item = std::io::Result<Bytes>> + Send + Sync + 'static,
from_size_bytes: usize,
to: &RemotePath,
metadata: Option<StorageMetadata>,
cancel: &CancellationToken,
) -> anyhow::Result<()> {
let kind = RequestKind::Put;
let _permit = self.permit(kind, cancel).await?;
let started_at = start_measuring_requests(kind);
View on GitHub (pinned to 8f60b04da4)
Solutions
- Compare the timestamp in the error message with aws s3api head-object for the same key
- Fix the emulator/uploader so Last-Modified is a valid post-epoch RFC 3339/HTTP date
- Overwrite or delete the malformed object if the endpoint cannot be fixed
Example fix
// before: emulator writes epoch-relative stamps
response.insert_header("Last-Modified", "Wed, 31 Dec 1969 23:59:59 GMT");
// after: emit a real UTC time
response.insert_header("Last-Modified", httpdate::fmt_http_date(std::time::SystemTime::now())); Defensive patterns
Strategy: try-catch
Validate before calling
use aws_sdk_s3::Client;
async fn last_modified_in_range(client: &Client, bucket: &str, key: &str) -> anyhow::Result<bool> {
let head = client.head_object().bucket(bucket).key(key).send().await?;
Ok(head.last_modified
.map(|t| t.secs() >= 0)
.unwrap_or(false))
} Type guard
fn is_time_conversion_error(err: &remote_storage::DownloadError) -> bool {
matches!(err, remote_storage::DownloadError::Other(e)
if e.to_string().contains("can't convert time"))
} Try / catch
match storage.list_files(&prefix, mode, &cancel).await {
Err(DownloadError::Other(e)) if e.to_string().contains("can't convert time") => {
// the message embeds the offending timestamp; fix or remove that object/endpoint
}
other => other?,
} Prevention
- Keep NTP/RTC correct on hosts that write objects
- Assert emulators emit post-epoch Last-Modified in their test suites
- Log the key along with the error so the malformed object can be located
When it happens
Trigger: An S3-compatible endpoint reporting a zero, negative, or otherwise out-of-range Last-Modified for a key being listed, e.g. an emulated default of 1970-01-01 minus an offset, or a gateway with broken clock handling.
Common situations: Emulators defaulting timestamps incorrectly; objects written by tooling with timezone math bugs; systems with wrong RTC clocks.
Related errors
- Missing ETag header
- Missing LastModified header
- head_object doesn't contain last_modified or content_length
- not implemented
- Read back file doesn't match original
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/8b4066994bad00cf.
Report an issue: GitHub.