Repository navigation
fix(service-storage): presigned S3 PUT URLs no longer carry the CRC32 of an empty body - #21164
Conversation
…arams (WIP, red on base) Offline pin driving the real S3Client and presigner with fake credentials and a fixed endpoint. Red against the unmodified adapter; the fix follows. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…, so presigned PUT URLs carry no empty-body CRC32 The SDK default (WHEN_SUPPORTED) stamps x-amz-checksum-crc32=AAAAAA== (the CRC32 of an empty body) and x-amz-sdk-checksum-algorithm=CRC32 into the signed query of a presigned PutObject, because a presign prepares the command with no body. Set requestChecksumCalculation and responseChecksumValidation to WHEN_REQUIRED on the adapter's S3Client; none of its commands is checksum-required. Pinned offline against the real SDK. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
… together Measured on the real SDK: the response option alone puts a CRC32 on a presigned GET URL, so the pair cannot be set one at a time. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8a1c73792d80dc4fe3289ddf438ccde9b70c1fe2 && git checkout 8a1c73792d80dc4fe3289ddf438ccde9b70c1fe2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ae1e95010a25350fb50c4939815387975810cfb1 33e30baf741d6106f3c1b092d8b2129d5cca3edf && git checkout -B drift-repro ae1e95010a25350fb50c4939815387975810cfb1 && git merge --no-ff 33e30baf741d6106f3c1b092d8b2129d5cca3edf
node scripts/docs-audit/affected-docs.mjs --json ae1e95010a25350fb50c4939815387975810cfb1 |
Fixes #21147
Clause-②: no
What changed
The S3 adapter's
S3Clientnow setsrequestChecksumCalculationandresponseChecksumValidationtoWHEN_REQUIRED(packages/services/service-storage/src/s3-storage-adapter.ts,getClient()). Before, the SDK default (WHEN_SUPPORTED) putx-amz-checksum-crc32=AAAAAA==(the CRC32 of an empty body) andx-amz-sdk-checksum-algorithm=CRC32into the signed query of every presigned PUT, because a presign prepares the command before any byte exists. A store that enforces a query-signed checksum against the uploaded body would refuse every browser upload while server-sideupload()kept working.Landing is exactly the card's file surface: the producer is this adapter, no other package builds an
S3Client.packages/services/service-storage/src/s3-storage-adapter.tspackages/services/service-storage/src/s3-storage-adapter.presign-checksum.test.ts.changeset/21147-s3-presign-no-checksum.mdpatchfor@objectstack/service-storage(not private;distships the option, grep hit indist/index.jsanddist/index.cjs)3 files, +164 / -1.
Evidence
Red before, green after. The new test drives the real
S3Clientand the real presigner, fully offline: fake credentials, fixed endpointhttp://127.0.0.1:9000, no socket opened. A test that mocks@aws-sdk/client-s3(ass3-storage-adapter.key-prefix.test.tsdoes) cannot see this defect, because the checksum is stamped by the SDK's own middleware.origin/main's adapter (base70dae533), the new file: 2 failed.expected [ 'x-amz-checksum-crc32', …(1) ] to deeply equal [], receivedx-amz-checksum-crc32,x-amz-sdk-checksum-algorithm.expected [ 'x-amz-checksum-mode' ] to deeply equal [].Tests 2 passed (2); whole packagepnpm --filter @objectstack/service-storage test:Test Files 41 passed (41),Tests 629 passed (629);typecheck(including the test-layer check) exit 0.Ablation of the pin (committed fix at HEAD, each leg through
scripts/ablation-replace.mjs: anchor hit once, blob hash moved, restore proven by blob equal to HEAD and an emptygit diff HEAD). The subject resolves tosrc/, not a dependency'sdist/, so no rebuild leg applies.crc32,sdk-checksum-algorithm)checksum-mode)requestChecksumCalculationonlycrc32on the GET URL)responseChecksumValidationonlychecksum-mode)The two options are a pair. Presigning with
@aws-sdk/client-s33.1090.0 under each combination:Setting only the response option puts a CRC32 on a GET URL that had none, so the pair is set together and the test header records the table.
What changes for
upload()and the other doors (measured against a loopback server over both plain HTTP and a self-signed HTTPS listener, same result): a server-sidePutObjectwith a Buffer body and anUploadPartno longer sendx-amz-sdk-checksum-algorithm/x-amz-checksum-crc32;x-amz-content-sha256(the SigV4 payload hash) is identical before and after, and the request still succeeds. AGetObjectno longer sendsx-amz-checksum-mode: ENABLED, sodownload()stops asking for and validating a response checksum.getPresignedDownloadloses thex-amz-checksum-mode=ENABLEDquery parameter. This is the pre-flexible-checksum behaviour; the SDK's default added it. None of the nine commands the adapter issues is checksum-required: in@aws-sdk/client-s33.1090.0requestChecksumRequired: trueis bound to bucket-config, ACL, tagging, retention,DeleteObjectsand similar commands, andPutObject,GetObject,DeleteObject,HeadObject,ListObjectsV2and the four multipart commands are not among them. Peer range is^3.0.0: SDKs older than the flexible-checksum release ignore the two options and never stamped a checksum.Local verification (HEAD
33e30baf)pnpm --workspace-concurrency=2 --filter '@objectstack/service-storage^...' buildexit 0 (direction: upstream dependencies of this package, the closure the test imports throughdist).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 63 commands, each run with its exit code recorded;--ranreconciliation:63 derived famil(ies) accounted for — 61 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3). 61 exited 0.pnpm check:dual-build-cjs-loadsandpnpm check:i18n, both exit 3,PREREQUISITE NOT MET(they read built output of dozens of packages outside this closure; CI builds them). Substitute for the first, narrower than the gate:pnpm --filter @objectstack/service-storage buildexit 0 andnode -e "require('./dist/index.cjs')"loads,S3StorageAdapteris a function. The diff touches no translation, extract config or metadata form, socheck:i18n's population is not moved by it, but that is a reading of the diff, not a measurement.AGENTS.md; restored withgit checkout HEAD -- AGENTS.md, never staged.Optional live leg
Not done. The repo has no container pattern for an enforcing store: no testcontainers or localstack dependency and no MinIO service in any workflow (
git grephits for MinIO are config prose and settings labels only). Whether AWS S3 or MinIO enforce a query-signed checksum stays NOT MEASURED, as on the card; this change removes the dependence on the answer.Acceptance notes
s3-storage-adapter.key-prefix.test.ts.Generated by Claude Code