|
| 1 | +# S3 compatibility upgrades: aws-chunked, signed payloads, multipart upload |
| 2 | + |
| 3 | +Date: 2026-07-06. Approved scope: features 1, 2, 4 from the compatibility |
| 4 | +review; all defaults confirmed (signed payloads always-on, trailers parsed |
| 5 | +and discarded, four core multipart operations). |
| 6 | + |
| 7 | +## Goal |
| 8 | + |
| 9 | +A stock AWS CLI/SDK with no custom configuration can upload through the |
| 10 | +edge's S3-compatible API: `aws s3api put-object` works with default |
| 11 | +checksum/signing settings, and `aws s3 cp` of files >8 MiB works via |
| 12 | +multipart upload. |
| 13 | + |
| 14 | +## Feature 1 — aws-chunked upload decoding (edge) |
| 15 | + |
| 16 | +Modern clients send upload bodies as |
| 17 | +`x-amz-content-sha256: STREAMING-UNSIGNED-PAYLOAD-TRAILER` with |
| 18 | +aws-chunked framing and a checksum trailer (e.g. `x-amz-checksum-crc32`). |
| 19 | + |
| 20 | +- The edge accepts this mode for upload operations (PutObject, UploadPart, |
| 21 | + CompleteMultipartUpload body). `X-Amz-Decoded-Content-Length` is |
| 22 | + required and becomes the object/part content length. |
| 23 | +- Unsigned aws-chunked framing is identical to HTTP/1.1 chunked framing; |
| 24 | + decoding wraps `httputil.NewChunkedReader` plus exact decoded-length |
| 25 | + enforcement (`internal/s3api`). |
| 26 | +- Trailers are parsed for framing validity and discarded. By the time the |
| 27 | + trailer arrives the payload has already streamed to the backend, so a |
| 28 | + checksum mismatch cannot fail the upload; this is documented. |
| 29 | +- Signed-chunk streaming modes (`STREAMING-AWS4-HMAC-SHA256-PAYLOAD*`) |
| 30 | + remain rejected: clients use them only on plain-HTTP endpoints and the |
| 31 | + edge is HTTPS-only. |
| 32 | +- One body-resolution step in the edge feeds both the routed |
| 33 | + (upload-source) and direct-server paths. |
| 34 | + |
| 35 | +## Feature 2 — signed-payload uploads (edge) |
| 36 | + |
| 37 | +`ValidatePayloadHashForOperation` additionally accepts a 64-hex-digit |
| 38 | +payload hash for mutation operations. The hash is covered by the verified |
| 39 | +SigV4 signature; the edge does not re-hash the streaming body (impossible |
| 40 | +without buffering). Transport integrity remains TLS's job. Always-on, no |
| 41 | +config flag. Documented as "accepted but not verified". |
| 42 | + |
| 43 | +## Feature 4 — multipart upload |
| 44 | + |
| 45 | +Operations: CreateMultipartUpload, UploadPart, CompleteMultipartUpload, |
| 46 | +AbortMultipartUpload — the set `aws s3 cp` needs. ListParts and |
| 47 | +ListMultipartUploads are deferred. |
| 48 | + |
| 49 | +- **Classification** (`internal/s3api`): by query params — POST `?uploads` |
| 50 | + → Create; PUT `?partNumber&uploadId` → UploadPart; POST `?uploadId` → |
| 51 | + Complete; DELETE `?uploadId` → Abort. |
| 52 | +- **Gating**: all four are mutations, gated by `MUTATIONS_ENABLED` on both |
| 53 | + edge and connector, same as PutObject/DeleteObject. |
| 54 | +- **Tickets** (`internal/tickets`): new operations plus a `Multipart` |
| 55 | + envelope: `UploadID`, `PartNumber`, and `Rewrite{Bucket,Key}` carrying |
| 56 | + the public names for response XML. Part lists never ride NATS tickets, |
| 57 | + keeping message size bounded. |
| 58 | +- **Body transport**: UploadPart streams its body through the existing |
| 59 | + upload-source channel exactly like PutObject. CompleteMultipartUpload's |
| 60 | + part-list XML travels the same way, capped at 1 MiB (S3 allows at most |
| 61 | + 10,000 parts ≈ <1 MiB of XML). |
| 62 | +- **Backend calls** (`internal/s3fetch`): SDK CreateMultipartUpload / |
| 63 | + UploadPart (UNSIGNED-PAYLOAD, same as PutObject) / Complete (parses the |
| 64 | + XML via `internal/s3api`) / Abort. Create and Complete responses are |
| 65 | + rendered deterministically with public bucket/key (same pattern as the |
| 66 | + ListObjectsV2 rewrite). |
| 67 | +- **Statuses**: Create 200 + XML, UploadPart 200 + ETag header, |
| 68 | + Complete 200 + XML, Abort 204. |
| 69 | +- **Direct-server aliases** get all four operations through the shared |
| 70 | + s3fetch code (edge-only gate, as with other direct mutations). |
| 71 | + |
| 72 | +## Error handling |
| 73 | + |
| 74 | +- Malformed chunked framing or decoded-length mismatch surfaces as a read |
| 75 | + error mid-stream; the backend PUT aborts (short body) and the client |
| 76 | + receives the standard stream-failure error. |
| 77 | +- Unknown/oversized Complete bodies → 400 InvalidRequest at the edge. |
| 78 | +- Multipart requests while mutations are disabled → 405, `Allow` |
| 79 | + reflecting the gate. |
| 80 | + |
| 81 | +## Testing |
| 82 | + |
| 83 | +Unit tests per package (chunked decoder incl. malformed framing and length |
| 84 | +mismatches; classification; ticket validation; s3fetch operations against |
| 85 | +stub servers; edge/connector wiring). End-to-end: compose stack driven by |
| 86 | +a default-configured `aws` CLI — put-object without custom config, and |
| 87 | +`aws s3 cp` of a >8 MiB file (multipart), plus abort and gate-off probes. |
| 88 | + |
| 89 | +## Docs |
| 90 | + |
| 91 | +README limitations updated (multipart/aws-chunked/signed-payload lines), |
| 92 | +`docs/configuration.md` operation list, and a short "Using the AWS CLI" |
| 93 | +note. `deploy/scripts/smoke.sh` gains default-config and multipart checks. |
0 commit comments