Skip to content

fix: preserve leading-slash S3 keys#78

Merged
digizeph merged 1 commit into
mainfrom
fix/leading-slash-r2-keys
Jul 22, 2026
Merged

fix: preserve leading-slash S3 keys#78
digizeph merged 1 commit into
mainfrom
fix/leading-slash-r2-keys

Conversation

@digizeph

Copy link
Copy Markdown
Member

Summary

Fixes the Cloudflare R2 403 regression for S3 object keys that begin with /.

Closes #77.

Changes

  • Preserve leading-slash keys when passing object paths to rusty-s3, whose Url::join() handling otherwise drops the path-style bucket component.
  • Apply the workaround consistently to read, single/multipart upload, multipart abort/complete, HEAD, delete, and copy destination operations.
  • Add ignored R2 integration coverage for leading-slash single-PUT and multipart upload/read/stat/delete flows.
  • Add an Unreleased changelog entry.

Testing

  • cargo fmt --check
  • cargo build --no-default-features
  • cargo build --all-features
  • cargo test --all-features
  • cargo clippy --all-features -- -D warnings
  • cargo clippy --no-default-features
  • cargo test --test s3_integration --features s3,rustls -- --ignored --test-threads=1 (15 R2 tests)

Release

No release-workflow change is needed: .github/workflows/release.yml triggers on v[0-9]+.* pushes, then format-checks, creates the GitHub Release from CHANGELOG.md, uploads CLI assets, and publishes to crates.io. Recent tag releases, including v0.23.0, completed successfully.

Copilot AI review requested due to automatic review settings July 22, 2026 16:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes an S3/R2 regression where object keys starting with / could be signed/constructed incorrectly (leading to 403s), by introducing a consistent key-munging workaround when constructing rusty-s3 actions/URLs. This fits into oneio’s S3 backend (src/s3) by ensuring request URL construction remains compatible with path-style endpoints such as Cloudflare R2.

Changes:

  • Add s3_action_key() and apply it across S3 read/write/head/delete/multipart/copy-destination code paths to avoid Url::join() dropping the path-style bucket component.
  • Add ignored Cloudflare R2 integration tests covering single-PUT and multipart flows using leading-slash keys.
  • Add an Unreleased changelog entry documenting the fix.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/s3/mod.rs Adds s3_action_key() and uses it broadly to preserve correct URL construction/signing for leading-slash keys.
tests/s3_integration.rs Adds ignored R2 integration tests for leading-slash key single-PUT and multipart flows.
CHANGELOG.md Documents the leading-slash key fix under [Unreleased].

Comment thread tests/s3_integration.rs
Comment on lines +142 to +145
assert_stream_matches(&bucket, &key, &data);

cleanup_test_objects(&bucket, &prefix);
}
Comment thread tests/s3_integration.rs
Comment on lines +217 to +220
assert_stream_matches(&bucket, &key, &data);

cleanup_test_objects(&bucket, &prefix);
}
Comment thread src/s3/mod.rs Outdated
Comment on lines +70 to +82
/// Make an S3 object key safe to pass to rusty-s3's URL resolver.
///
/// `rusty_s3::Bucket::object_url()` uses `Url::join()`, which treats a key
/// beginning with `/` as an absolute URL path and drops the path-style bucket
/// component. Prefixing a relative-path marker preserves the key's leading
/// slash in the resulting request path.
fn s3_action_key(key: &str) -> Cow<'_, str> {
if key.starts_with('/') {
Cow::Owned(format!(".{key}"))
} else {
Cow::Borrowed(key)
}
}
Copilot AI review requested due to automatic review settings July 22, 2026 17:02
@digizeph
digizeph force-pushed the fix/leading-slash-r2-keys branch from 015d783 to e7102c1 Compare July 22, 2026 17:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

Comment thread src/s3/mod.rs Outdated
Comment on lines +105 to +110
let datetime = query
.iter()
.find(|(name, _)| name == "X-Amz-Date")
.map(|(_, value)| value.as_str())
.ok_or_else(|| OneIoError::NotSupported("Missing X-Amz-Date in S3 action".to_string()))?;
let datestamp = &datetime[..8];
Comment thread src/s3/mod.rs Outdated
Comment on lines +91 to +97
let endpoint: reqwest::Url = config
.endpoint
.parse()
.map_err(|e| OneIoError::NotSupported(format!("Invalid S3 endpoint: {e}")))?;
let endpoint_path = endpoint.path().trim_end_matches('/');
url.set_path(&format!("{endpoint_path}/{}/{}", config.bucket, key));

Comment thread src/s3/mod.rs Outdated
Comment thread src/s3/mod.rs
Comment on lines +166 to +181
fn s3_object_url(config: &config::S3Config, key: &str) -> Result<reqwest::Url, OneIoError> {
if !key.starts_with('/') || !uses_path_style(config) {
return config
.rusty_bucket()?
.object_url(key)
.map_err(|e| OneIoError::NotSupported(format!("Invalid object key: {e}")));
}

let mut endpoint: reqwest::Url = config
.endpoint
.parse()
.map_err(|e| OneIoError::NotSupported(format!("Invalid S3 endpoint: {e}")))?;
let endpoint_path = endpoint.path().trim_end_matches('/');
endpoint.set_path(&format!("{endpoint_path}/{}/{}", config.bucket, key));
Ok(endpoint)
}
Copilot AI review requested due to automatic review settings July 22, 2026 20:42
@digizeph
digizeph force-pushed the fix/leading-slash-r2-keys branch from e7102c1 to 0bfb178 Compare July 22, 2026 20:42
@digizeph

Copy link
Copy Markdown
Member Author

Addressed the current Copilot feedback in 0bfb178:

  • Added explicit leading-slash s3_delete / s3_exists == false assertions to both single-PUT and multipart regressions.
  • Added a non-ignored unit test that asserts path-style URLs preserve both the bucket component and the literal leading slash, including an escaped key.
  • Rebuild paths from rusty-s3's already-encoded action URL path rather than raw key input.
  • Return an error for malformed X-Amz-Date instead of slicing unchecked.
  • Use the canonical encoded query string directly when attaching the replacement SigV4 signature.

Local checks and all 16 live R2 integration tests pass; CI has been re-triggered.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@digizeph
digizeph merged commit b1a81d7 into main Jul 22, 2026
7 checks passed
@digizeph
digizeph deleted the fix/leading-slash-r2-keys branch July 22, 2026 22:00
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.

fix: preserve leading-slash S3 keys with Cloudflare R2

2 participants