From e12100b4a1017d737213a703c48843e9090b5c22 Mon Sep 17 00:00:00 2001 From: cxymds Date: Tue, 22 Sep 2026 11:18:59 +0800 Subject: [PATCH] fix(s3): emit x-amz-expiration as HTTP-date (#8057) The expiry-date field of the x-amz-expiration response header was rendered as RFC 3339 (e.g. 2026-10-02T00:00:00Z). AWS SDKs parse this value with an RFC 822 parser; the Java SDK v1 rejects the ISO-8601 shape, drops the whole header, and logs a WARN from ObjectExpirationHeaderHandler. S3 specifies HTTP-date (RFC 1123 with a literal GMT zone), which is also what MinIO emits. Render via the shared HTTP-date formatter (extracted alongside format_expires_header) and pin the exact output in the unit test. --- rustfs/src/app/object/shared.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) 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]