Skip to content

fix: return clear 400 for keyless writes instead of misleading sha256 error#168

Open
alukach wants to merge 2 commits into
mainfrom
worktree-clearer-keyless-write-error
Open

fix: return clear 400 for keyless writes instead of misleading sha256 error#168
alukach wants to merge 2 commits into
mainfrom
worktree-clearer-keyless-write-error

Conversation

@alukach

@alukach alukach commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Problem

Uploading to a product without an object key fails with a baffling error:

$ aws s3 cp README.md s3://alukach/private-bucket
upload failed: ... An error occurred (InvalidRequest) when calling the
PutObject operation: The value of x-amz-content-sha256 header is invalid.

The sha256 header is a red herring. With no trailing slash, the AWS CLI can't
tell private-bucket is a "folder", so it uploads the file as the object
literally named private-bucket — i.e. account=alukach, product=private-bucket,
object key = empty. The proxy maps that keyless /{account}/{product} path to
a bucket-level request and forwards a PUT with a streaming (aws-chunked)
payload to the bucket root. The upstream rejects a streaming-payload
x-amz-content-sha256 on a keyless PUT, surfacing the confusing header complaint.

Adding a trailing slash or the full key works (s3://alukach/private-bucket/),
but the error gives no hint as to why.

Fix

Short-circuit PUT/DELETE to a keyless path (/{account}/{product} or
shorter) with a clear 400 InvalidRequest that names the real cause:

<Error>
  <Code>InvalidRequest</Code>
  <Message>Missing object key: a PUT must address an object at /{account}/{product}/{key}, not the product root.</Message>
  <Resource>/alukach/private-bucket</Resource>
  ...
</Error>

POST is intentionally left alone — keyless POST /{account}/{product}?delete
(multi-object delete) is a legitimate bucket-level operation.

Notes

  • Modeled on the existing fail-closed STS short-circuit in fetch.
  • No unit test: the lib is wasm-only with [lib] test = false, so it can't be
    exercised by cargo test. The guard is a one-line predicate over the existing,
    production-exercised extract_path_segments.
  • Verified locally: cargo fmt --check, cargo clippy --target wasm32-unknown-unknown -- -D warnings, and cargo test all pass.

🤖 Generated with Claude Code

… error

A PUT/DELETE to `/{account}/{product}` (no object key) was forwarded upstream
and rejected with the confusing "x-amz-content-sha256 header is invalid".
This happens easily with `aws s3 cp f s3://account/product` (no trailing
slash): the CLI uploads `f` as the object literally named `product`, so the
request carries a streaming-payload content-sha256 against a keyless path.

Short-circuit PUT/DELETE to a keyless path with a 400 InvalidRequest that
names the real cause — the missing object key — so callers stop chasing the
sha256 red herring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @alukach's task in 59s —— View job


✅ No blocking issues — safe to merge.

  • object_path.rs:41is_none_or is stable since Rust 1.82 (Oct 2024); confirm the project's MSRV allows it. Not a blocker if CI passes.
  • object_path.rs:14-23trim_matches('/') strips all leading/trailing slashes, so extract_path_segments can never yield Some("") for the key slot. The is_none_or(str::is_empty) branch for the empty-string case is therefore dead code — is_none() alone would suffice. Minor; no behaviour difference.
  • lib.rs:165 — guard fires before authz, so an unauthenticated caller gets the 400 InvalidRequest rather than a 403 Forbidden. Leaking that the path structure is "invalid" to an unauthed caller is harmless here (the path is in the URL anyway), and failing early with a clear message is the right trade-off.
  • tests/object_path.rs:6 — the #[path] trick mirrors tests/authz.rs; the test re-compiles the module natively, bypassing [lib] test = false. Pattern is sound; just worth noting it bypasses the crate's wasm32 compilation so wasm-only imports in object_path.rs would silently break tests. The module is currently import-free, so no issue.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

🚀 Latest commit deployed to https://source-data-proxy-pr-168.source-coop.workers.dev

  • Date: 2026-06-26T16:45:33Z
  • Commit: 9076021

Extract `extract_path_segments` and the new `is_keyless_write` predicate into
a wasm-free `object_path` module so they can be unit-tested natively via the
`#[path]` pattern already used by `tests/authz.rs` (the lib is `cdylib` with
`[lib] test = false`). Adds `tests/object_path.rs` covering segment parsing,
keyless PUT/DELETE, keyed writes, and the read / multi-object-delete cases
that must not be flagged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant