diff --git a/rustfs/src/app/object/shared.rs b/rustfs/src/app/object/shared.rs index 62c7095ec..421c47cdf 100644 --- a/rustfs/src/app/object/shared.rs +++ b/rustfs/src/app/object/shared.rs @@ -673,7 +673,10 @@ fn build_put_object_expiration_header(event: &lifecycle::Event) -> Option) -> S3Result S3Result { + format_http_date(expires) +} + +/// Render a date-valued S3 response header (`Expires`, `x-amz-expiration`) as +/// HTTP-date: RFC 1123 with a literal `GMT` zone, e.g. +/// `Fri, 23 Dec 2012 00:00:00 GMT`. +/// +/// Interoperability depends on this shape. `x-amz-expiration` in particular is +/// parsed with an RFC 822 parser by the AWS SDKs, which rejects an RFC 3339 +/// value such as `2026-10-02T00:00:00Z` and then discards the whole header. +pub(super) fn format_http_date(timestamp: &Timestamp) -> S3Result { let mut formatted = Vec::new(); - expires + timestamp .format(TimestampFormat::HttpDate, &mut formatted) - .map_err(|e| ApiError::from(StorageError::other(format!("Invalid expires timestamp: {e}"))))?; + .map_err(|e| ApiError::from(StorageError::other(format!("Invalid HTTP-date timestamp: {e}"))))?; Ok(String::from_utf8_lossy(&formatted).into_owned()) } @@ -1873,9 +1887,10 @@ mod tests { storage_class: String::new(), }; - let expiry_date = expire_time.format(&Rfc3339).unwrap(); - let expected = format!("expiry-date=\"{}\", rule-id=\"rule-1\"", expiry_date); - assert_eq!(build_put_object_expiration_header(&event), Some(expected)); + // HTTP-date, not RFC 3339: the AWS SDKs parse this field with an + // RFC 822 parser, so `2026-10-02T00:00:00Z` loses the metadata. + let expected = "expiry-date=\"Tue, 14 Nov 2023 22:13:20 GMT\", rule-id=\"rule-1\""; + assert_eq!(build_put_object_expiration_header(&event), Some(expected.to_string())); } #[test]