{"record":{"id":"55293dae5c10e402","repo":"rustfs/rustfs","slug":"failed-to-format-signing-timestamp-reason-55293d","errorCode":null,"errorMessage":"failed to format signing timestamp: {reason}","messagePattern":"failed to format signing timestamp: (.+?)","errorType":"exception","errorClass":"SignV4Error","httpStatus":null,"severity":"error","filePath":"crates/signer/src/request_signature_v4.rs","lineNumber":39,"sourceCode":"use std::sync::LazyLock;\nuse time::{OffsetDateTime, macros::format_description};\nuse tracing::warn;\n\nuse super::constants::UNSIGNED_PAYLOAD;\nuse super::request_signature_streaming_unsigned_trailer::streaming_unsigned_v4;\nuse super::utils::{HostAddrError, sign_v4_trim_all, try_get_host_addr};\nuse rustfs_utils::crypto::{hex, hex_sha256, hmac_sha256};\nuse s3s::Body;\n\npub const SIGN_V4_ALGORITHM: &str = \"AWS4-HMAC-SHA256\";\npub const SERVICE_TYPE_S3: &str = \"s3\";\npub const SERVICE_TYPE_STS: &str = \"sts\";\n\n#[derive(Debug, thiserror::Error)]\npub enum SignV4Error {\n    #[error(\"invalid UTF-8 header value for `{name}`\")]\n    InvalidHeaderValue { name: String },\n    #[error(\"failed to format signing timestamp: {reason}\")]\n    TimeFormat { reason: String },\n    #[error(\"failed to build signing timestamp: {reason}\")]\n    TimeComponent { reason: String },\n    #[error(\"failed to encode query parameters: {reason}\")]\n    QueryEncode { reason: String },\n    #[error(\"failed to parse uri: {reason}\")]\n    InvalidUri { reason: String },\n    #[error(\"failed to build uri from parts: {reason}\")]\n    InvalidUriParts { reason: String },\n    #[error(\"failed to convert canonical headers to UTF-8: {reason}\")]\n    CanonicalUtf8 { reason: String },\n    #[error(\"failed to parse header value for `{name}`: {reason}\")]\n    HeaderValueParse { name: String, reason: String },\n}\n\npub type SignResult<T> = std::result::Result<T, SignV4Error>;\n\n#[derive(Debug)]","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/rustfs/rustfs/blob/35af688cd9d41b4346fbe27dcf7250ba72046c1f/crates/signer/src/request_signature_v4.rs#L21-L57","documentation":"SignV4Error::TimeFormat comes from format_amz_datetime (request_signature_v4.rs:84-88), which renders the signing instant as ISO 'YYYYMMDDThhmmssZ' using a compiled format_description. try_pre_sign_v4 accepts the timestamp t as a caller parameter, so an OffsetDateTime whose components fall outside the format's representable range (classically a year beyond four digits from a bad unix-timestamp conversion) is the realistic trigger. The header-signing path uses now_utc() and cannot hit it on a sane clock; streaming signing (request_signature_streaming.rs:65,164) reuses the same conversion.","triggerScenarios":"Calling try_pre_sign_v4/pre_sign_v4 with t built from unvalidated arithmetic (e.g. expiry = now + i64::MAX seconds then from_unix_timestamp), or a test fixture constructing OffsetDateTime with an out-of-range component. now_utc()-based calls only fail with a hardware clock centuries off.","commonSituations":"Presign-URL generation services computing expires timestamps in milliseconds-vs-seconds confusion, producing year-50000+ datetimes; property tests sweeping extreme timestamps; clock skew after VM migration. Symptom: presign returns the typed error (try_) or a warn! plus an invalid/unsigned URL (non-try).","solutions":["Clamp or validate the computed OffsetDateTime before passing it: reject years outside 0..=9999.","Fix unit confusion: ensure the expires arithmetic uses seconds, not milliseconds.","Use try_pre_sign_v4 so the failure carries the reason string rather than silently producing a broken URL."],"exampleFix":"// before\nlet t = OffsetDateTime::from_unix_timestamp(expires_secs).unwrap_or(OffsetDateTime::MAX); // year 99999+ -> [year] format fails\n\n// after\nlet t = OffsetDateTime::from_unix_timestamp(expires_secs)\n    .ok()\n    .filter(|t| (0..=9999).contains(&t.year()))\n    .ok_or_else(|| anyhow::anyhow!(\"expiry out of representable range\"))?;","handlingStrategy":"validation","validationCode":"fn datetime_formattable(t: time::OffsetDateTime) -> bool {\n    (0..=9999).contains(&t.year())\n        && t.hour() <= 23 && t.minute() <= 59 && t.second() <= 59\n}\nanyhow::ensure!(datetime_formattable(t), \"signing timestamp outside formattable range\");","typeGuard":"fn is_time_format(e: &SignV4Error) -> bool {\n    matches!(e, SignV4Error::TimeFormat { .. })\n}","tryCatchPattern":"Err(SignV4Error::TimeFormat { reason }) => {\n    // caller-supplied timestamp had out-of-range components; recompute from now\n    tracing::warn!(%reason, \"bad signing timestamp, falling back to now\");\n    return try_pre_sign_v4(req, ak, sk, token, region, expires, time::OffsetDateTime::now_utc())\n        .map_err(Into::into);\n}","preventionTips":["Validate expiry arithmetic (seconds, not milliseconds) before converting to OffsetDateTime.","Clamp or reject years outside 0..=9999 when accepting caller-supplied timestamps.","Use try_pre_sign_v4 so the format failure carries its reason string."],"tags":["rust","rustfs","s3","signing","sigv4","presign","time","timestamp"],"backgroundTag":"timestamp-formatting-error","analyzedSha":"35af688cd9d41b4346fbe27dcf7250ba72046c1f","analyzedAt":"2026-08-20T21:57:04.799Z","contentChangedAt":"2026-08-20T21:57:04.799Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}