fix(s3): default CopyObject content-type to binary/octet-stream on REPLACE (#8073)

fix: default CopyObject content-type to binary/octet-stream on REPLACE
This commit is contained in:
Tyler Hillery
2026-09-23 13:04:05 -05:00
committed by GitHub
parent 713920dcd6
commit 624463c5ba
2 changed files with 28 additions and 5 deletions
@@ -298,7 +298,11 @@ mod tests {
assert_eq!(empty_head_resp.content_disposition(), None);
assert_eq!(empty_head_resp.content_encoding(), None);
assert_eq!(empty_head_resp.content_language(), None);
assert_eq!(empty_head_resp.content_type(), None);
assert_eq!(
empty_head_resp.content_type(),
Some("binary/octet-stream"),
"REPLACE without an explicit content type must default to binary/octet-stream, matching S3"
);
assert_eq!(empty_head_resp.expires_string(), None);
assert_eq!(empty_head_resp.website_redirect_location(), None);
@@ -394,7 +398,15 @@ mod tests {
assert_eq!(head.content_disposition(), (field == "content-disposition").then_some("attachment"));
assert_eq!(head.content_encoding(), (field == "content-encoding").then_some("gzip"));
assert_eq!(head.content_language(), (field == "content-language").then_some("de"));
assert_eq!(head.content_type(), (field == "content-type").then_some("text/field"));
assert_eq!(
head.content_type(),
Some(if field == "content-type" {
"text/field"
} else {
"binary/octet-stream"
}),
"REPLACE without an explicit content type must default to binary/octet-stream, matching S3"
);
assert_eq!(
head.expires_string(),
(field == "expires").then_some(replacement_expires_http_date.as_str())
@@ -420,7 +432,11 @@ mod tests {
.send()
.await
.expect("HEAD failed for metadata collision case");
assert_eq!(collision_head.content_type(), None);
assert_eq!(
collision_head.content_type(),
Some("binary/octet-stream"),
"user-metadata under the reserved 'content-type' key must not satisfy the system content-type default"
);
assert_eq!(collision_head.content_encoding(), None);
assert_eq!(
collision_head.metadata().and_then(|metadata| metadata.get("content-type")),
+9 -2
View File
@@ -607,9 +607,16 @@ impl DefaultObjectUsecase {
// Handle MetadataDirective REPLACE: replace user metadata while preserving system metadata.
// System metadata (compression, encryption) is added after this block to ensure
// it's not cleared by the REPLACE operation.
if let Some(replacement_metadata) = replacement_metadata {
if let Some(mut replacement_metadata) = replacement_metadata {
// S3 defaults an object's content type to `binary/octet-stream` when none is
// supplied. REPLACE must apply that same default instead of leaving
// content-type unset.
if !replacement_metadata.contains_key("content-type") {
replacement_metadata.insert("content-type".to_string(), "binary/octet-stream".to_string());
}
let effective_content_type = replacement_metadata.get("content-type").cloned();
user_defined = replacement_metadata;
src_info.content_type = content_type.clone();
src_info.content_type = effective_content_type;
src_info.content_encoding = content_encoding.as_deref().and_then(normalize_content_encoding_for_storage);
src_info.expires = expires_timestamp.map(OffsetDateTime::from);
} else if metadata_directive.is_some() || website_redirect_location.is_some() {