Skip to content

Fix ranged GET signing failure with Cloudflare R2#452

Open
w4nderlust wants to merge 1 commit intodurch:masterfrom
w4nderlust:fix/ranged-get-signing-r2
Open

Fix ranged GET signing failure with Cloudflare R2#452
w4nderlust wants to merge 1 commit intodurch:masterfrom
w4nderlust:fix/ranged-get-signing-r2

Conversation

@w4nderlust
Copy link
Copy Markdown

@w4nderlust w4nderlust commented Apr 6, 2026

Problem

get_object_range (ranged GET) requests fail with SignatureDoesNotMatch on Cloudflare R2.

The headers() method in request_trait.rs had a skip-list of commands that should not include content-length and content-type headers. GetObjectRange was missing from this list, so it fell through to the catch-all _ => arm that adds content-length: 0 and content-type: text/plain.

These empty/meaningless headers get included in the AWS4-HMAC-SHA256 canonical request. AWS S3 happens to tolerate this, but Cloudflare R2 rejects the signature because the canonical request doesn't match what R2 computes server-side.

The same class of bug also affected other body-less commands routed through PUT/POST/DELETE (DeleteObject, AbortMultipartUpload, InitiateMultipartUpload, etc.).

Fix

Added a has_body() helper on Command that returns true only for commands that actually serialize request content (PutObject, UploadPart, CompleteMultipartUpload, CreateBucket, PutBucketLifecycle, PutBucketCors, PutObjectTagging). The header insertion now checks has_body() instead of using a skip-list or matching on the HTTP verb. This prevents the same bug from happening if new body-less commands are added in the future.

Testing

All existing tests pass. Verified the fix against a real Cloudflare R2 bucket: ranged GET downloads that were failing with SignatureDoesNotMatch now succeed.


This change is Reviewable

Summary by CodeRabbit

Refactor

  • Optimized S3 API request header handling to improve consistency and maintainability across different command types.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 25c25286-b238-447b-b4f0-dbf6333a33bd

📥 Commits

Reviewing files that changed from the base of the PR and between e32c578 and 3ae1dd0.

📒 Files selected for processing (2)
  • s3/src/command.rs
  • s3/src/request/request_trait.rs

📝 Walkthrough

Walkthrough

Replaced per-variant header branching with logic that inserts Content-Length and Content-Type only when Command::has_body() is true; Command::has_body() was added. CopyObject handling kept separate, inserting only x-amz-copy-source. x-amz-content-sha256 and x-amz-date unchanged.

Changes

Cohort / File(s) Summary
Request header construction
s3/src/request/request_trait.rs
Removed explicit per-Command match for omitting body headers; now inserts CONTENT_LENGTH/CONTENT_TYPE only when self.command().has_body() is true. Kept x-amz-content-sha256 and x-amz-date. Refactored CopyObject to an if let that inserts only x-amz-copy-source.
Command body indicator
s3/src/command.rs
Added pub fn has_body(&self) -> bool returning true for body-bearing commands (PutObject, PutObjectTagging, UploadPart, CompleteMultipartUpload, CreateBucket, PutBucketLifecycle, PutBucketCors) and false for others.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇 I hopped through code with steady cheer,
Moved content headers where they're clear,
Copy-source kept its single note,
Bodies flag the headers we wrote,
A carrot-coded change — now done, my dear! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main change: fixing a ranged GET signing failure with Cloudflare R2, which is the core issue resolved by this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@brownian-motion-v0
Copy link
Copy Markdown

Brownian Motion (Brass)

Recommendation: Proceed

Summary: Fixes signing issue for ranged GET requests with Cloudflare R2.
Risk: Medium · Confidence: 80%

Highlights

  • Good test coverage
  • Clear commit history

Unknowns

  • No linked issue

Next actions

  • Keep: the change to check http_verb() for headers
  • Drop: the previous skip-list approach
  • Add: documentation on the new approach for future maintainers

Reflection questions

  • What core assumption underpins this PR's approach?
  • How does this change align with the project's longer-term goals?
  • Could there be a simpler way to achieve the primary objective here?

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
s3/src/request/request_trait.rs (1)

703-719: ⚠️ Potential issue | 🟠 Major

http_verb() is still too broad a proxy for signing body headers.

This fixes the GET/HEAD case, but Lines 712-719 still sign Content-Length and Content-Type for zero-body commands like CopyObject, AbortMultipartUpload, DeleteObject, DeleteBucket, and InitiateMultipartUpload because they are PUT/POST/DELETE. request_body() on Lines 231-254 treats those as empty, so the same signature-drift class can still happen on providers that normalize or ignore empty-request Content-* headers. Please drive this off a dedicated “does this command serialize a body?” helper instead of the HTTP verb.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@s3/src/request/request_trait.rs` around lines 703 - 719, The code currently
decides to sign Content-Length/Content-Type based on http_verb(), which still
signs headers for zero-body commands (e.g., CopyObject, AbortMultipartUpload,
DeleteObject, DeleteBucket, InitiateMultipartUpload); instead, check whether the
command actually serializes a body (use an existing request_body() or add a
helper like serializes_body()/has_request_body() on the Command) and only insert
CONTENT_LENGTH/CONTENT_TYPE when that helper returns true; update the block that
now matches on self.command().http_verb() to call
self.command().request_body().is_some() (or the new helper) and keep
special-case CopyObject header handling as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@s3/src/request/request_trait.rs`:
- Around line 703-719: The code currently decides to sign
Content-Length/Content-Type based on http_verb(), which still signs headers for
zero-body commands (e.g., CopyObject, AbortMultipartUpload, DeleteObject,
DeleteBucket, InitiateMultipartUpload); instead, check whether the command
actually serializes a body (use an existing request_body() or add a helper like
serializes_body()/has_request_body() on the Command) and only insert
CONTENT_LENGTH/CONTENT_TYPE when that helper returns true; update the block that
now matches on self.command().http_verb() to call
self.command().request_body().is_some() (or the new helper) and keep
special-case CopyObject header handling as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 12ead844-6852-400e-923b-8eb7e89e5af0

📥 Commits

Reviewing files that changed from the base of the PR and between 3a9314f and 3eb5fdf.

📒 Files selected for processing (1)
  • s3/src/request/request_trait.rs

@w4nderlust w4nderlust force-pushed the fix/ranged-get-signing-r2 branch from 3eb5fdf to e32c578 Compare April 7, 2026 05:23
@brownian-motion-v0
Copy link
Copy Markdown

Brownian Motion (Brass)

Recommendation: Proceed

Summary: Fixes a signing issue with ranged GET requests for Cloudflare R2.
Risk: Medium · Confidence: 80%

Highlights

  • Good test coverage
  • Addresses a specific compatibility issue

Unknowns

  • No linked issue
  • Potential edge cases with new commands

Next actions

  • Keep: The new has_body() method for clarity.
  • Drop: The skip-list approach for header management.
  • Add: Documentation on the rationale for the changes.

Reflection questions

  • What core assumption underpins this PR's approach?
  • How does this change align with the project's longer-term goals?
  • Could there be a simpler way to achieve the primary objective here?

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@s3/src/command.rs`:
- Around line 214-227: The headers() implementation in request_trait.rs still
checks HTTP verb via self.command().http_verb() to decide whether to insert
CONTENT_LENGTH and CONTENT_TYPE; change that logic to call
self.command().has_body() instead so headers are added for commands that
actually carry bodies (refer to the Command::has_body() method you added).
Replace the current match on HttpMethod with an if self.command().has_body() {
insert CONTENT_LENGTH and CONTENT_TYPE } branch, ensuring you use the same
header keys (CONTENT_LENGTH, CONTENT_TYPE) and value computation already
present; also add a short doctest/example to the public has_body() method’s doc
comment showing typical usage (e.g., calling Command::has_body() to gate header
insertion).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 936ed796-81ba-470d-9dc3-4b663a75cb5a

📥 Commits

Reviewing files that changed from the base of the PR and between 3eb5fdf and e32c578.

📒 Files selected for processing (2)
  • s3/src/command.rs
  • s3/src/request/request_trait.rs
✅ Files skipped from review due to trivial changes (1)
  • s3/src/request/request_trait.rs

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.
@w4nderlust w4nderlust force-pushed the fix/ranged-get-signing-r2 branch from e32c578 to 3ae1dd0 Compare April 7, 2026 05:38
@brownian-motion-v0
Copy link
Copy Markdown

Brownian Motion (Brass)

Recommendation: Proceed

Summary: Fixes signing issue for body-less commands in Cloudflare R2 requests.
Risk: Medium · Confidence: 80%

Highlights

  • Good test coverage
  • Clear commit history

Unknowns

  • No linked issue
  • Missing performance metrics on signing efficiency

Next actions

  • Keep: the has_body() helper for future-proofing against new commands.
  • Drop: the skip-list approach which adds unnecessary complexity.
  • Add: documentation on the new has_body() method for clarity.

Reflection questions

  • What core assumption underpins this PR's approach?
  • How does this change align with the project's longer-term goals?
  • Could there be a simpler way to achieve the primary objective here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant