diff --git a/s3/src/command.rs b/s3/src/command.rs index c56ca3ee46..3c5a6c2dde 100644 --- a/s3/src/command.rs +++ b/s3/src/command.rs @@ -211,6 +211,21 @@ impl<'a> Command<'a> { } } + /// Whether this command carries a request body that should be reflected + /// in `Content-Length` and `Content-Type` headers during signing. + pub fn has_body(&self) -> bool { + matches!( + self, + Command::PutObject { .. } + | Command::PutObjectTagging { .. } + | Command::UploadPart { .. } + | Command::CompleteMultipartUpload { .. } + | Command::CreateBucket { .. } + | Command::PutBucketLifecycle { .. } + | Command::PutBucketCors { .. } + ) + } + pub fn content_length(&self) -> Result { let result = match &self { Command::CopyObject { from: _ } => 0, diff --git a/s3/src/request/request_trait.rs b/s3/src/request/request_trait.rs index 9199689102..6e76aecc83 100644 --- a/s3/src/request/request_trait.rs +++ b/s3/src/request/request_trait.rs @@ -700,23 +700,22 @@ pub trait Request { headers.insert(HOST, host_header.parse()?); - match self.command() { - Command::CopyObject { from } => { - headers.insert(HeaderName::from_static("x-amz-copy-source"), from.parse()?); - } - Command::ListObjects { .. } => {} - Command::ListObjectsV2 { .. } => {} - Command::GetObject => {} - Command::GetObjectTagging => {} - Command::GetBucketLocation => {} - Command::ListBuckets => {} - _ => { - headers.insert( - CONTENT_LENGTH, - self.command().content_length()?.to_string().parse()?, - ); - headers.insert(CONTENT_TYPE, self.command().content_type().parse()?); - } + if let Command::CopyObject { from } = self.command() { + headers.insert(HeaderName::from_static("x-amz-copy-source"), from.parse()?); + } + + // Only include content-length and content-type for commands that + // actually carry a request body. Body-less commands (GET, HEAD, + // DELETE, CopyObject, AbortMultipartUpload, etc.) must not have + // these headers in the signed request, otherwise providers like + // Cloudflare R2 reject the signature because the empty + // content-length value corrupts the canonical request. + if self.command().has_body() { + headers.insert( + CONTENT_LENGTH, + self.command().content_length()?.to_string().parse()?, + ); + headers.insert(CONTENT_TYPE, self.command().content_type().parse()?); } headers.insert( HeaderName::from_static("x-amz-content-sha256"),