Repository navigation
Commit ca167f6
fix(service-storage)!: the chunked-upload init door parses its body with the declared request schema (#27781)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DDaLcR4LnipNjp4uwkB7Ze2)_
Fixes #27549
Clause-②: no (narrowing — the init door parses its body with
`InitiateChunkedUploadRequestSchema`; a `chunkSize` below 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 enforces
Card part (a) only. Part (b) (`GetPresignedUrlRequestSchema.size`) was
split to `domain:spec` by triage 6498840027 and is not touched here.
## 维护者速读
- 改了什么:分块上传的开启接口 `POST /api/v1/storage/upload/chunked` 现在用规范里已声明的
`InitiateChunkedUploadRequestSchema` 解析请求体,不合规就 400 拒收,点名出错字段。
- 为什么改:以前只看 `totalSize` 是否为真值,负数或字符串也能开出一个永远无法完成的上传会话;小于 5 MiB 的
`chunkSize` 被悄悄抬到 5 MiB,而规范说的是拒收。
- 风险与回滚:收窄接受面,传字符串 `totalSize` 或小于 5 MiB `chunkSize` 的第三方客户端会开始收到
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。
- 需要你做的:无,席位落地(契约复审 PASS 后)。
## What changes
`POST /api/v1/storage/upload/chunked`
(`packages/services/service-storage/src/storage-routes.ts`) now parses
its body with `InitiateChunkedUploadRequestSchema` before the pending
`sys_file` row, the backend multipart and the `sys_upload_session` row
are written. The handler then reads the parsed values.
- **Refused `400 INVALID_REQUEST`**, naming each failing field with the
schema's own words, with neither row written: a `totalSize` that is not
a whole JSON number of at least 1 (`-5`, `"100"`, `0`, `1.5`); a
`chunkSize` that is not a whole JSON number of at least 5242880 (it is
no longer clamped up); a `bucket` that is not a string; a `metadata`
that is not an object of string values.
- **Unchanged:** an omitted `chunkSize` takes the schema's declared
default of 5242880, so a valid body mints the same session with the same
`totalChunks`. A missing `filename`, `mimeType` or `totalSize` keeps its
existing message, and an off-vocabulary `scope` keeps its own remedy:
the scope gate still runs before the parse. The presence check for
`totalSize` moves from truthiness to `== null` (the presigned door's
shape), so `0` now reaches the parse and is refused by name.
- Same envelope as the presigned door's refusal from PR #27537:
`sendError(res, 400, 'INVALID_REQUEST', message)`. No new error code.
- **ADR-0087 disposition:** `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 for `objectstack migrate meta` to rewrite.
The changeset carries the BREAKING banner and the FROM / TO / Fix
account.
## Why
`InitiateChunkedUploadRequestSchema` declares `totalSize: int().min(1)`
and `chunkSize: int().min(5242880)`, and the door enforced neither
(declared is not enforced). A negative total minted a session with
`totalChunks` of `-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 a `chunkSize`
below the minimum is refused, not clamped.
## Tests
`packages/services/service-storage/src/storage-routes.test.ts`, in
`Chunked upload flow`:
- 7 `REFUSED 400 INVALID_REQUEST` rows: negative, numeric-string, zero
and fractional `totalSize`; `chunkSize` 1024; numeric-string
`chunkSize`; a non-string `metadata` value. Each asserts `status`,
`success: false`, `error.code`, that the message names the field, and
that `createFile` and `createSession` were never called.
- 3 `CONTROL` rows: an omitted `chunkSize` (default 5242880, 3 chunks
for 12 000 000 bytes), `chunkSize` at the minimum (3 chunks) and above
it (10485760, 2 chunks). Each asserts the response's `chunkSize` /
`totalChunks`, the stored session's `chunk_size` / `total_chunks`, and
the stored file size.
<details><summary>Ablation: drop the parse, then restore</summary>
Run with `node scripts/ablation-replace.mjs` on `storage-routes.ts`,
after the fix was committed (`e3c83f13`). Anchor `const 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 from `6b8c79453e4b` to `86146bd7a141`.
- Mutated: `Tests 12 failed | 58 passed (70)`. All 7 REFUSED rows and
the omitted-`chunkSize` CONTROL went red, plus 4 existing
terminal-status tests whose init omits `chunkSize` (a `NaN` chunk count
without the schema default). The two CONTROL rows that send a
`chunkSize` stayed green, as expected.
- Restored: blob after restore `6b8c79453e4b` equals the HEAD blob, and
`git diff HEAD` is empty. Then `Tests 70 passed (70)`.
The subject is imported from `src/` (`./storage-routes.js`), so no dist
rebuild sits between the mutation and the run.
</details>
## Acceptance notes
- The parse covers the whole declared body, as the ruling says, so
`bucket` and `metadata` narrow too (listed in the changeset). `null` for
`chunkSize`, `bucket` or `metadata` was read as omitted and is now
refused, because the schema declares them optional, not nullable.
- `filename: ''` and `mimeType: ''` stay refused by the existing
presence check. That check is stricter than the schema's `z.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`.
<details><summary>Package tests and typecheck</summary>
- `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.
</details>
<details><summary>Derived gate families (dispatch-gates --commands, 64
families)</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 64 families from the 3 changed paths
at `26e6d217`. All 64 exited 0. `check:i18n` and
`check:dual-build-cjs-loads` first exited 3 (prerequisite not met) and
passed after the build closure they name: `check:i18n` OK, 10 packages
in sync; `check:dual-build-cjs-loads` 112 entry points across 69
packages load. `--ran` reconciliation, 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)`.
</details>
<details><summary>Dogfood battery</summary>
Full battery once on the final head `26e6d217` (title carries `!`,
`Clause-②:` carries the narrowing arm): build closure
`@objectstack/dogfood^...`, then `pnpm --filter @objectstack/dogfood
test` under 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.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4d1JR8d3KVqut8R6TWN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3cc4563 commit ca167f6
4 files changed
Lines changed: 117 additions & 10 deletions
File tree
- .changeset
- docs/qa/platform-checklist/areas
- packages/services/service-storage/src
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
808 | 808 | | |
809 | 809 | | |
810 | 810 | | |
811 | | - | |
| 811 | + | |
812 | 812 | | |
813 | 813 | | |
814 | 814 | | |
| |||
823 | 823 | | |
824 | 824 | | |
825 | 825 | | |
826 | | - | |
| 826 | + | |
827 | 827 | | |
828 | 828 | | |
829 | 829 | | |
| |||
882 | 882 | | |
883 | 883 | | |
884 | 884 | | |
885 | | - | |
| 885 | + | |
886 | 886 | | |
887 | 887 | | |
888 | 888 | | |
| |||
903 | 903 | | |
904 | 904 | | |
905 | 905 | | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
906 | 912 | | |
907 | 913 | | |
908 | 914 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
255 | 255 | | |
256 | 256 | | |
257 | 257 | | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
258 | 308 | | |
259 | 309 | | |
260 | 310 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
9 | 11 | | |
10 | 12 | | |
11 | 13 | | |
| |||
174 | 176 | | |
175 | 177 | | |
176 | 178 | | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
177 | 191 | | |
178 | 192 | | |
179 | 193 | | |
| |||
1100 | 1114 | | |
1101 | 1115 | | |
1102 | 1116 | | |
1103 | | - | |
1104 | | - | |
| 1117 | + | |
| 1118 | + | |
1105 | 1119 | | |
1106 | 1120 | | |
1107 | 1121 | | |
1108 | 1122 | | |
1109 | | - | |
1110 | | - | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
| 1126 | + | |
| 1127 | + | |
| 1128 | + | |
| 1129 | + | |
| 1130 | + | |
| 1131 | + | |
| 1132 | + | |
| 1133 | + | |
| 1134 | + | |
| 1135 | + | |
1111 | 1136 | | |
1112 | 1137 | | |
1113 | 1138 | | |
1114 | 1139 | | |
1115 | | - | |
| 1140 | + | |
1116 | 1141 | | |
1117 | | - | |
1118 | 1142 | | |
1119 | 1143 | | |
1120 | 1144 | | |
| |||
0 commit comments