Commit 55d17c0
fix(proxy): stream-resign aws-chunked uploads instead of decoding (#92)
* fix(proxy): stream-resign aws-chunked uploads instead of decoding
Modern AWS clients (aws-cli >= 2.23, recent SDKs) frame PutObject and
UploadPart bodies as `Content-Encoding: aws-chunked` with a streaming sentinel
(`x-amz-content-sha256: STREAMING-...`). The proxy forwarded them untouched —
PutObject via a presigned URL (UNSIGNED-PAYLOAD), UploadPart via a re-signed
raw request — and S3 only de-chunks a request signed with a matching streaming
sentinel, so it stored the raw chunk envelope (`7\r\nhello!\n\r\n0\r\n<trailer>`)
as the object. Every aws-chunked write was corrupted.
For the common unsigned-payload variant (`STREAMING-UNSIGNED-PAYLOAD-TRAILER`,
the client default), re-sign only the request seed with the backend
credentials — reusing the client's streaming sentinel and the de-chunk headers
(content-encoding, x-amz-decoded-content-length, x-amz-trailer) — and stream
the chunk framing straight through for S3 to de-chunk. Zero-copy: the worker
never buffers or hashes the payload, which matters on the Cloudflare Workers
memory ceiling. PutObject and UploadPart share this path.
Signed-chunk uploads (`STREAMING-AWS4-HMAC-SHA256-PAYLOAD`) carry per-chunk
signatures bound to the client's key that can't be re-signed to the backend
credentials, so they are rejected with NotImplemented (501) rather than
silently corrupted.
Content-Length is forwarded but left unsigned: the runtime owns the streaming
transfer framing, and S3 sizes the payload from x-amz-decoded-content-length
and the chunk framing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(proxy): reject aws-chunked streaming uploads on non-S3 backends
Review follow-up to the stream-resign change. Three fixes from a 4-agent
review:
- Bug: the PutObject streaming arm reached `build_streaming_forward`
(which hardcodes S3 seed signing) without a backend-type guard, so a
default aws-chunked PUT to a non-S3 backend (azure) mis-routed into S3
signing instead of rejecting — unlike UploadPart, which guards earlier.
Guard at the point the S3 assumption is made; reject with InvalidRequest.
- Drop the unreachable `Internal` re-read of x-amz-content-sha256: the
classifier already validated it, so it now returns the sentinel for the
caller to thread through as the payload hash.
- Strengthen the UploadPart test to assert partNumber/uploadId survive into
the forwarded URL, and add a non-S3 streaming-rejection regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(proxy): trim aws-chunked over-engineering leftovers
Ponytail over-engineering pass on the aws-chunked streaming work:
- Delete the unused `STREAMING_PAYLOAD` const — zero references repo-wide;
the new `aws_chunked` module classifies the sentinel by substring, so the
literal is dead.
- Consolidate the non-S3 backend gate into `try_streaming_forward` so
`build_streaming_forward` becomes a pure constructor. Behavior unchanged:
PutObject streaming to a non-S3 backend still rejects 400, covered by
`streaming_put_on_non_s3_backend_is_rejected`.
- Drop two redundant `aws_chunked` unit assertions — the signed `-TRAILER`
variant and the hex-hash case each re-hit a branch already covered.
No behavior change. cargo fmt / clippy -D warnings / test (111 passing) /
wasm32 check all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(proxy): forward flexible-checksum headers on multipart ops (#93)
* fix(proxy): forward flexible-checksum headers on multipart ops
Modern AWS SDKs/CLI (botocore / CLI v2 >= ~2.23) enable CRC32 data-integrity
checksums by default. For a multipart upload that means:
- CreateMultipartUpload sends `x-amz-checksum-algorithm` (declares the MPU's
checksum algorithm), and
- CompleteMultipartUpload echoes the per-part / full-object checksums via
`x-amz-checksum-type` / `x-amz-checksum-crc32` plus the per-part values in
the XML body.
`execute_multipart` only forwarded `content-type`/`content-length`/`content-md5`
and dropped every `x-amz-checksum-*` header. So the MPU was initialized with no
checksum context while parts were stored *with* CRC32 checksums (the streaming
re-sign path forwards the `x-amz-trailer`), and S3 rejected
CompleteMultipartUpload with `InvalidPart`:
An error occurred (InvalidPart) when calling the CompleteMultipartUpload
operation: One or more of the specified parts could not be found...
Forward the client's `x-amz-checksum-*` and `x-amz-sdk-checksum-algorithm`
headers on this raw-signed path. It signs every header present
(`sign_s3_request`), so they are covered by the signature — unlike the
presigned PutObject path, where unsigned `x-amz-*` headers are rejected. This
also fixes a plain (non-chunked) UploadPart carrying a header checksum, which
routes through the same function.
Workaround for clients until deployed: set
`AWS_REQUEST_CHECKSUM_CALCULATION=when_required`.
Adds a functional regression test driving `handle_with_body` with a
CompleteMultipartUpload and a header-capturing backend, asserting the checksum
headers both reach the backend and appear in the re-signed SignedHeaders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(smoke): multipart upload regression test against real S3
The dropped-checksum bug (InvalidPart on CompleteMultipartUpload) only
reproduces against real AWS S3. Verified empirically that MinIO RELEASE.2025-09
does NOT reproduce it: an MPU created without a checksum algorithm but with
CRC32 parts + completion succeeds on MinIO and is rejected by S3. So the
existing MinIO integration multipart tests can't guard this — a real-S3 smoke
test is required.
Add `TestMultipartUpload` to the smoke suite: a checksum-forced
(`ChecksumAlgorithm=CRC32`) two-part upload via boto3's transfer manager
(mirroring `aws s3 cp`), asserting the round-trip. It is gated on
`SMOKE_WRITE_BUCKET`, so it self-skips until a writable bucket is configured.
Wire a `smoke-writable` bucket + a github-actions write scope into
wrangler.deploy.toml (OIDC-federated, no long-lived keys). Activation needs the
backend bucket created, the OIDC role granted S3 write perms, and
SMOKE_WRITE_BUCKET=smoke-writable set in the smoke job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(smoke): reuse multistore-test-bucket for the write smoke test
Point the smoke-writable bucket at the existing federated-test backend bucket
(multistore-test-bucket) under a smoke-writable/ prefix instead of a separate
bucket. The role already has PutObject/GetObject/DeleteObject/ListBucket
(covering Create/Upload/Complete); only s3:AbortMultipartUpload need be added so
failed uploads clean up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: pass SMOKE_WRITE_BUCKET to the smoke-test job
Wires the multipart write smoke test (TestMultipartUpload) to run on every
deploy that exercises the smoke suite — including per-PR preview deploys, not
just main. Gated behind the SMOKE_WRITE_BUCKET repo/environment variable: unset
(default) -> the test self-skips; set to smoke-writable -> it runs against the
freshly-deployed worker and real S3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: run the multipart write smoke test on every preview
Drop the opt-in repo-variable gate; instead pass smoke_write_bucket as a
deploy.yml input and have preview.yml set it to smoke-writable unconditionally.
Every PR preview now exercises the real-S3 multipart upload (no main deploy
needed); staging/production leave the input empty, so the test self-skips there
unless they opt in the same way.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent b0f5b7f commit 55d17c0
8 files changed
Lines changed: 651 additions & 34 deletions
File tree
- .github/workflows
- crates/core/src
- backend
- examples/cf-workers
- tests/smoke
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
23 | 28 | | |
24 | 29 | | |
25 | 30 | | |
| |||
112 | 117 | | |
113 | 118 | | |
114 | 119 | | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
115 | 124 | | |
116 | 125 | | |
117 | 126 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
| 25 | + | |
24 | 26 | | |
25 | 27 | | |
26 | 28 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
155 | | - | |
156 | | - | |
157 | | - | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| |||
0 commit comments