Skip to content

Commit e32c578

Browse files
committed
Fix ranged GET signing failure with Cloudflare R2
GetObjectRange (and other body-less commands like DeleteObject, AbortMultipartUpload, etc.) were getting content-length and content-type headers included in the signed request. For commands with no body these headers are empty/meaningless, but they still end up in the canonical request signature. Cloudflare R2 rejects the resulting signature (SignatureDoesNotMatch), while AWS S3 happens to tolerate it. Added a `has_body()` helper on Command that returns true only for commands that actually serialize request content (PutObject, UploadPart, CompleteMultipartUpload, etc.). The header insertion in request_trait.rs now checks `has_body()` instead of matching on the HTTP verb, which avoids signing empty headers for any current or future body-less command.
1 parent 3a9314f commit e32c578

File tree

2 files changed

+27
-11
lines changed

2 files changed

+27
-11
lines changed

s3/src/command.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,21 @@ impl<'a> Command<'a> {
211211
}
212212
}
213213

214+
/// Whether this command carries a request body that should be reflected
215+
/// in `Content-Length` and `Content-Type` headers during signing.
216+
pub fn has_body(&self) -> bool {
217+
matches!(
218+
self,
219+
Command::PutObject { .. }
220+
| Command::PutObjectTagging { .. }
221+
| Command::UploadPart { .. }
222+
| Command::CompleteMultipartUpload { .. }
223+
| Command::CreateBucket { .. }
224+
| Command::PutBucketLifecycle { .. }
225+
| Command::PutBucketCors { .. }
226+
)
227+
}
228+
214229
pub fn content_length(&self) -> Result<usize, S3Error> {
215230
let result = match &self {
216231
Command::CopyObject { from: _ } => 0,

s3/src/request/request_trait.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use url::Url;
1111

1212
use crate::LONG_DATETIME;
1313
use crate::bucket::Bucket;
14-
use crate::command::Command;
14+
use crate::command::{Command, HttpMethod};
1515
use crate::error::S3Error;
1616
use crate::signing;
1717
use bytes::Bytes;
@@ -700,16 +700,17 @@ pub trait Request {
700700

701701
headers.insert(HOST, host_header.parse()?);
702702

703-
match self.command() {
704-
Command::CopyObject { from } => {
705-
headers.insert(HeaderName::from_static("x-amz-copy-source"), from.parse()?);
706-
}
707-
Command::ListObjects { .. } => {}
708-
Command::ListObjectsV2 { .. } => {}
709-
Command::GetObject => {}
710-
Command::GetObjectTagging => {}
711-
Command::GetBucketLocation => {}
712-
Command::ListBuckets => {}
703+
if let Command::CopyObject { from } = self.command() {
704+
headers.insert(HeaderName::from_static("x-amz-copy-source"), from.parse()?);
705+
}
706+
707+
// Only include content-length and content-type for methods that carry
708+
// a request body. GET and HEAD requests must not have these headers in
709+
// the signed request, otherwise providers like Cloudflare R2 reject
710+
// the signature (the empty content-length value corrupts the canonical
711+
// request).
712+
match self.command().http_verb() {
713+
HttpMethod::Get | HttpMethod::Head => {}
713714
_ => {
714715
headers.insert(
715716
CONTENT_LENGTH,

0 commit comments

Comments
 (0)