Repository navigation
fix(service-storage)!: the chunked-upload init door parses its body with the declared request schema - #27781
Conversation
…ith the declared request schema POST /storage/upload/chunked checked totalSize for truthiness only and clamped chunkSize up to the 5 MiB minimum. A negative or string total minted a session no chunk could ever complete. The body is now parsed with InitiateChunkedUploadRequestSchema before anything is stored: a total that is not a positive whole number, a chunkSize below the declared minimum, or any other off-schema field is refused 400 INVALID_REQUEST naming the field. An omitted chunkSize keeps the declared 5242880 default. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1JR8d3KVqut8R6TWN
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1JR8d3KVqut8R6TWN
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 10 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 9481bcf3b15ba2e3b00d7c7f6c43276c44a5f187 && git checkout 9481bcf3b15ba2e3b00d7c7f6c43276c44a5f187
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 968cacfd10fdec64b6089d0da4f721f1a8a10925 e77ad2750539a20766866f78c526b8f78b9d2516 && git checkout -B drift-repro 968cacfd10fdec64b6089d0da4f721f1a8a10925 && git merge --no-ff e77ad2750539a20766866f78c526b8f78b9d2516
node scripts/docs-audit/affected-docs.mjs --json 968cacfd10fdec64b6089d0da4f721f1a8a10925
|
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head 26e6d21 · 2026-10-29T23:02Z |
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL Contract reviewServed-tier: Read-only shape held: the diff ( Check-runs on the head as read at 2026-10-29T23:13Z: success — Build Core, Check Changeset (this job runs ① Derived judgmentsThe diff changes ONE door,
② Semver level
③ Boundary flagsDev report
④ Design judgmentThe narrowing makes the spec's own refusal real; it takes no declared capability away. Every shape the door now refuses is a shape Trying to break it at the consumers: every in-repo caller of the init door sends Findings:
Implemented-by: VERDICT: PASS |
…not the 5 MiB floor Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1JR8d3KVqut8R6TWN
|
domain:server#4 · os-elon-musk · session_016WV4d1JR8d3KVqut8R6TWN Delta for contract review 6510724527's should-fix, new head
PR body correction (a dev does not re-write the PR body, so it is recorded here): the Generated by Claude Code |
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head e77ad27 · 2026-10-29T23:20Z |
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL Contract reviewServed-tier: Read-only shape held: the delta diff, the card's and the PR's comments since that record, the head's check-runs. Nothing built, run or re-run. One head-tree JSON was parsed offline to read two numbers (the item's Check-runs on ① Derived judgmentsThe delta touches one JSON ledger under ② Semver levelUnchanged by the delta: no published package is touched, the changeset ③ Boundary flags
④ Design judgmentThe delta closes record 6510724527's one should-fix, and the rewritten text holds against the code it describes at this head:
Findings:
Implemented-by: VERDICT: PASS |
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL 维护者速读
席位意见:ACCEPT。 席位核对Head-sha:
|
Requested by Elon · project thread
Fixes #27549
Clause-②: no (narrowing — the init door parses its body with
InitiateChunkedUploadRequestSchema; achunkSizebelow the declared minimum is refused 400, not clamped, per triage 6498840027)Metadata-protocol: none
Skills-docs: none — no skill or doc teaches the old clamp or a string total; the generated reference (
content/docs/references/api/storage.mdx) already states the schema the door now enforcesCard part (a) only. Part (b) (
GetPresignedUrlRequestSchema.size) was split todomain:specby triage 6498840027 and is not touched here.维护者速读
POST /api/v1/storage/upload/chunked现在用规范里已声明的InitiateChunkedUploadRequestSchema解析请求体,不合规就 400 拒收,点名出错字段。totalSize是否为真值,负数或字符串也能开出一个永远无法完成的上传会话;小于 5 MiB 的chunkSize被悄悄抬到 5 MiB,而规范说的是拒收。totalSize或小于 5 MiBchunkSize的第三方客户端会开始收到 400;SDK 按同一类型发送不受影响。回滚即还原本 PR 的单个 commit。packages/services/service-storage/src/storage-routes.ts(init 处理器的解析与拒收)、storage-routes.test.ts(新钉子)、.changeset/27549-chunked-init-declared-shape.md。pnpm --filter @objectstack/service-storage exec vitest run src/storage-routes.test.ts,预期 70/70 通过,其中 7 条 REFUSED、3 条 CONTROL。What changes
POST /api/v1/storage/upload/chunked(packages/services/service-storage/src/storage-routes.ts) now parses its body withInitiateChunkedUploadRequestSchemabefore the pendingsys_filerow, the backend multipart and thesys_upload_sessionrow are written. The handler then reads the parsed values.400 INVALID_REQUEST, naming each failing field with the schema's own words, with neither row written: atotalSizethat is not a whole JSON number of at least 1 (-5,"100",0,1.5); achunkSizethat is not a whole JSON number of at least 5242880 (it is no longer clamped up); abucketthat is not a string; ametadatathat is not an object of string values.chunkSizetakes the schema's declared default of 5242880, so a valid body mints the same session with the sametotalChunks. A missingfilename,mimeTypeortotalSizekeeps its existing message, and an off-vocabularyscopekeeps its own remedy: the scope gate still runs before the parse. The presence check fortotalSizemoves from truthiness to== null(the presigned door's shape), so0now reaches the parse and is refused by name.sendError(res, 400, 'INVALID_REQUEST', message). No new error code.not-required (no-migration-prescription). A REST request body is refused against a spec schema that already declares it, unchanged; no metadata key, export, type or error code moves, so there is nothing forobjectstack migrate metato rewrite. The changeset carries the BREAKING banner and the FROM / TO / Fix account.Why
InitiateChunkedUploadRequestSchemadeclarestotalSize: int().min(1)andchunkSize: int().min(5242880), and the door enforced neither (declared is not enforced). A negative total minted a session withtotalChunksof-0, so every chunk was refused after #27412 and the upload could never complete; a numeric-string total was stored as sent. Triage ruled (6498840027) that declared is enforced and achunkSizebelow the minimum is refused, not clamped.Tests
packages/services/service-storage/src/storage-routes.test.ts, inChunked upload flow:REFUSED 400 INVALID_REQUESTrows: negative, numeric-string, zero and fractionaltotalSize;chunkSize1024; numeric-stringchunkSize; a non-stringmetadatavalue. Each assertsstatus,success: false,error.code, that the message names the field, and thatcreateFileandcreateSessionwere never called.CONTROLrows: an omittedchunkSize(default 5242880, 3 chunks for 12 000 000 bytes),chunkSizeat the minimum (3 chunks) and above it (10485760, 2 chunks). Each asserts the response'schunkSize/totalChunks, the stored session'schunk_size/total_chunks, and the stored file size.Ablation: drop the parse, then restore
Run with
node scripts/ablation-replace.mjsonstorage-routes.ts, after the fix was committed (e3c83f13). Anchorconst parsed = InitiateChunkedUploadRequestSchema.safeParse(body);replaced with a pass-through{ success: true, data: body }. The anchor went from 1 to 0 hits on disk and the blob from6b8c79453e4bto86146bd7a141.Tests 12 failed | 58 passed (70). All 7 REFUSED rows and the omitted-chunkSizeCONTROL went red, plus 4 existing terminal-status tests whose init omitschunkSize(aNaNchunk count without the schema default). The two CONTROL rows that send achunkSizestayed green, as expected.6b8c79453e4bequals the HEAD blob, andgit diff HEADis empty. ThenTests 70 passed (70).The subject is imported from
src/(./storage-routes.js), so no dist rebuild sits between the mutation and the run.Acceptance notes
bucketandmetadatanarrow too (listed in the changeset).nullforchunkSize,bucketormetadatawas read as omitted and is now refused, because the schema declares them optional, not nullable.filename: ''andmimeType: ''stay refused by the existing presence check. That check is stricter than the schema'sz.string(), and this PR leaves it as it was.scope ?? 'user'in the handler body is now redundant (the parse applies the declared default) and is left as it was, to keep the diff small.Verification
All local runs are on head
26e6d217.Package tests and typecheck
pnpm --filter @objectstack/service-storage test:Test Files 70 passed (70),Tests 1297 passed (1297), lock VERDICT command-exit 0.pnpm --filter @objectstack/service-storage typecheck: exit 0;check:test-typecheck: OK, 0 errors.pnpm lint: exit 0.Derived gate families (dispatch-gates --commands, 64 families)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 64 families from the 3 changed paths at26e6d217. All 64 exited 0.check:i18nandcheck:dual-build-cjs-loadsfirst exited 3 (prerequisite not met) and passed after the build closure they name:check:i18nOK, 10 packages in sync;check:dual-build-cjs-loads112 entry points across 69 packages load.--ranreconciliation, with an exit code on every line:64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero — all 64 recorded an exit code and none of them is 3).Dogfood battery
Full battery once on the final head
26e6d217(title carries!,Clause-②:carries the narrowing arm): build closure@objectstack/dogfood^..., thenpnpm --filter @objectstack/dogfood testunder the verify lock, no shard and no path.Test Files 355 passed | 3 skipped (358),Tests 2867 passed | 20 skipped (2887), lock VERDICT command-exit 0, held 21m00s. No failures.Human Minimum
packages/specholds no runtime logic. (packages/specnot touched)pnpm lintpasses.main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code