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

  1. Compare the timestamp in the error message with aws s3api head-object for the same key
  2. Fix the emulator/uploader so Last-Modified is a valid post-epoch RFC 3339/HTTP date
  3. 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

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


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