Repository navigation
Commit a68430a
fix(spec)!: the chunked-upload init request declares filename and mimeType non-empty (#27887)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiEosMC3zDfHaauURHFNq1GJ)_
Fixes #27801
Clause-②: no (narrowing)
Metadata-protocol: none
## 维护者速读
- 改了什么:分块上传初始化请求的 schema(`InitiateChunkedUploadRequestSchema`)现在声明
`filename`、`mimeType` 不能为空(`min(1)`),参考文档同步标注“non-empty”。
- 为什么改:上传入口一直拒收空文件名、空 MIME 类型(400 INVALID_REQUEST),但公开 schema
写的是“任意字符串”,按 schema 写代码的调用方或 AI 会被误导;现在声明与执行一致。
- 风险与回滚:上传入口的行为一字未改(状态码、错误码、文案、与 scope 检查的先后都不变),只有直接用这个 schema
解析空值的代码会从通过变成失败;回滚即 revert 本 PR。
- 先看:`packages/spec/src/api/storage.zod.ts`(两个
`min(1)`)、`packages/services/service-storage/src/storage-routes.ts`(入口的存在性检查为何保留的注释)、`.changeset/27801-chunked-init-filename-min1.md`(BREAKING
说明与 ADR-0087 处置)。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run
src/api/storage.test.ts`,预期 65 个测试全过,其中包括“refuses an empty filename /
mimeType”两条。
- 需要你做的:无,席位落地(等契约复审记录)。
Independent review: none run (quota); the PR waits for the seat's
contract-review hand-off.
## What changes
- `InitiateChunkedUploadRequestSchema.filename` and `.mimeType` declare
`min(1)`, and their descriptions read "Original filename (non-empty)"
and "File MIME type (non-empty)". The published JSON Schema now carries
`minLength: 1` on both keys, and
`content/docs/references/api/storage.mdx` is regenerated (2 lines).
- The door (`POST /api/v1/storage/upload/chunked`) is unchanged in
behaviour. Its presence check `!body.filename || !body.mimeType ||
body.totalSize == null` stays where it is. A comment now says that it
refuses nothing the schema parse accepts.
- Changeset `27801-chunked-init-filename-min1.md`, `@objectstack/spec`
patch, `fix(spec)!`, with both declaration lines and the ADR-0087
disposition `not-required (no-migration-prescription)`. This follows the
precedent of the other request-declaration narrowings
(`26817-http-find-query-params-count-select.md`,
`27683-search-fields-names-a-field.md`).
**What happens to the door's truthiness check.** I kept it, for three
measured reasons:
1. With `min(1)` the check is a strict subset of the schema's refusal
set. A missing, `''`, `null`, `0` or `false` value fails
`z.string().min(1)` as well, so the two never disagree about what is
refused.
2. It runs **before** the scope gate. The scope gate runs before the
parse, and #22470 asked for that order. If the check were folded into
the parse, an empty `filename` sent with an off-vocabulary `scope` would
get the scope refusal instead of the presence refusal. That is the
ordering change the order forbids.
3. Folding it in would also change the message for an empty or missing
`filename` / `mimeType` from `filename, mimeType, and totalSize are
required` to `chunkedInitRefusalMessage`'s text. A missing `totalSize`
would still need its own check ahead of the scope gate. Keeping the
check means status, code, message and ordering stay byte-identical,
which is what the ruling's "no behaviour change at the door" asks for.
## Tests
- `packages/spec/src/api/storage.test.ts`: `refuses an empty %s, naming
that key` covers `filename` and `mimeType`. Each asserts `success:
false`, exactly one issue, `path` equal to the key, and `code` equal to
`too_small`. `CONTROL: a one-character filename and mimeType parse`
asserts that `'a'` / `'b'` parse.
- `packages/services/service-storage/src/storage-routes.test.ts`:
`REFUSED 400 INVALID_REQUEST: an empty %s, which the declared schema
refuses too, with no row written` covers `filename` and `mimeType`. Each
asserts status `400`, `success: false` and `error.code`
`INVALID_REQUEST`, that `InitiateChunkedUploadRequestSchema.safeParse`
refuses the same body, and that neither `createFile` nor `createSession`
was called. No earlier test sent an empty `filename` or `mimeType` to
this door. The existing chunked-init cases cover `totalSize`,
`chunkSize` and `metadata` only.
<details><summary>Ablation (spec): removing each min(1) turns its own
case red</summary>
Run with `node scripts/ablation-replace.mjs`. The anchor hit once each
time and the file was restored to its HEAD blob (`e4b1a7313842`), with
`git diff HEAD` empty.
- `filename: z.string().min(1)` replaced by `filename: z.string()`: `×
refuses an empty filename, naming that key`, Tests 1 failed / 64 passed
(65).
- `mimeType: z.string().min(1)` replaced by `mimeType: z.string()`: `×
refuses an empty mimeType, naming that key`, Tests 1 failed / 64 passed
(65).
The door test is not ablated, because the door's behaviour did not
change. Its schema-agreement assertion reads `@objectstack/spec/api`
through the built `dist/`.
</details>
<details><summary>Local runs</summary>
- `pnpm --filter @objectstack/spec exec vitest run
src/api/storage.test.ts`: Test Files 1 passed, Tests 65 passed.
- `pnpm --filter @objectstack/service-storage test`: Test Files 71
passed (71), Tests 1311 passed (1311). `pnpm --filter
@objectstack/service-storage typecheck`: exit 0, and
`check:test-typecheck` OK.
- `pnpm --filter @objectstack/spec check:generated`: 1 of 14 stale
(`content/docs/references/**`) before `--fix`. `--fix` regenerated
`gen:docs` and re-checked green. After merging `origin/main`, the spec
was rebuilt and all 14 generated artefacts are up to date.
- Probe on the merged head, driving both doors and both schemas with
`''`:
- presigned `filename ''`: schema `success: true`, door `400
INVALID_REQUEST`.
- chunked `filename ''`: schema `success: false`, door `400
INVALID_REQUEST`.
- The same holds for `mimeType`.
</details>
## Acceptance notes
- **Out of scope, reported to the seat and not filed:**
`GetPresignedUrlRequestSchema.filename` and `.mimeType`
(`storage.zod.ts:67-68`) are still bare `z.string()`. `POST
/api/v1/storage/upload/presigned` refuses `''` for either with `400
INVALID_REQUEST` (`storage-routes.ts:968-969`), so this is the same
declared-versus-enforced gap on the sibling door. The probe above shows
it. That door does not parse its body with the schema at all. This
belongs with the card split from #27549 part (b).
- A whitespace-only `filename` / `mimeType` is still admitted by both
the schema and the door. The two agree, and this PR leaves that alone.
## Verification
All readings below are on head `f5bb94f94e`, which is the branch merged
with `origin/main` at `755e28500`.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 109 commands, and all 109 were run
with each exit code captured before any pipe.
- 106 exited 0 on the first pass.
- 3 exited 3 (PREREQUISITE NOT MET, unbuilt `dist/`):
`check:skill-examples`, `check:dual-build-cjs-loads` and `check:i18n`.
All 3 exited 0 after a workspace build.
- `--ran` reconciliation: "109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN
(a DERIVED zero — all 109 recorded an exit code and none of them is 3)".
- `node scripts/check-adr-0087-registration.mjs --base origin/main`
exited 0 and reads the changeset as `[BREAKING+bang+clause-②-narrowing]
not-required (no-migration-prescription)`.
- Full dogfood (`pnpm --filter @objectstack/dogfood test`, under the
lock, closure built) gave VERDICT command-exit 1: Test Files 3 failed /
352 passed / 3 skipped (358), Tests 4 failed / 2864 passed / 20 skipped
(2888). All 4 failures still fail when their files are run alone, and
none touches storage:
- 2 in `error-sentences-live.dogfood.test.ts` and 1 in
`object-hook-lifecycle-scratch.dogfood.test.ts` assert that the `/data`
door answers an undeclared hook abort with no `code`. Commit
`21c4bdd804` on `main` made that answer `HOOK_ABORTED`.
- 1 in `platform-app-object-entry-views.test.ts` names
`sys_exchange_rate` as undeclared. That object arrived on `main` with
`194e66c028`.
- This is main-red on the push-only battery, reported to the seat and
not caused by this diff.
- CI check-runs: not yet read. The PR opens as a draft, and the seat
reads convergence.
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2a3dec2 commit a68430a
6 files changed
Lines changed: 74 additions & 4 deletions
File tree
- .changeset
- content/docs/references/api
- packages
- services/service-storage/src
- spec/src/api
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
236 | 236 | | |
237 | 237 | | |
238 | 238 | | |
239 | | - | |
240 | | - | |
| 239 | + | |
| 240 | + | |
241 | 241 | | |
242 | 242 | | |
243 | 243 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
284 | 285 | | |
285 | 286 | | |
286 | 287 | | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
287 | 310 | | |
288 | 311 | | |
289 | 312 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1115 | 1115 | | |
1116 | 1116 | | |
1117 | 1117 | | |
| 1118 | + | |
| 1119 | + | |
| 1120 | + | |
| 1121 | + | |
1118 | 1122 | | |
1119 | 1123 | | |
1120 | 1124 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
338 | 338 | | |
339 | 339 | | |
340 | 340 | | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
341 | 363 | | |
342 | 364 | | |
343 | 365 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
194 | 194 | | |
195 | 195 | | |
196 | 196 | | |
197 | | - | |
198 | | - | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
199 | 201 | | |
200 | 202 | | |
201 | 203 | | |
| |||
0 commit comments