Skip to content

fix(rest): a refused xlsx export answers the 400 envelope, not a 200 workbook stream - #27888

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-nc7szf
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-nc7szf

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#4 · os-elon-musk · session_016WV4cyUucJoh7384vix5rh

Fixes #27755

维护者速读

  • 改了什么:导出接口选 Excel(xlsx)格式时,先读第一页数据,再开始生成工作簿;与 fix(rest): a refused json export answers the 400 envelope, not a truncated 200 stream #27753 对 json 的修法相同。
  • 为什么改:之前工作簿一打开就把响应定成 200,查询被拒(例如按不存在的字段搜索)时用户拿到的是一个坏的或空的 Excel 文件,而不是说明错误的 400。
  • 风险与回滚:改动只有导出函数里的一行挪位;成功导出的状态码、响应头和单元格内容不变。回滚即还原这一行。
  • 先看:packages/rest/src/data-action-routes.ts 导出循环里新增的 createXlsxStream 那一行。
  • 两分钟验证:GET /api/v1/data/OBJECT/export?format=xlsx&search=x&searchFields=nope 应回 400 INVALID_FIELD,与 csv 一致;不带错误参数的 xlsx 导出照常下载可打开的工作簿。
  • 需要你做的:无;席位复核通过、CI 全绿后按本席落地口径落地。

What changes

Why

Opening the exceljs streaming writer commits the Hono adapter to a streaming 200. Opening it before the first read meant a query that read refused reached the client as a 200 workbook download, while format=csv (and, since #27753, format=json) answered the 400 envelope. Triage 6510461172: the xlsx writer opens after the first page is read and answers the same envelope. A refusal on a later page is not widened here.

Verification

  • Pin, over a real SqlDriver (SQLite): ?search=x&searchFields=nope (400 INVALID_FIELD) and ?search=x&searchFields=, (400 VALIDATION_ERROR) answer the same status and body for format=xlsx as for format=csv. Ablation: on the unfixed code both xlsx rows are red (expected 200 to be 400); with the fix, green.
  • Preservation: a successful xlsx export over one page, three pages (page=50, 120 rows) and zero rows answers 200, the workbook content type, X-Export-Styles: applied, the same number of reads, and a workbook that loads with the same cells (header + rows; empty for zero rows). Green on the unfixed code too, so the success path did not move.
  • dispatch-gates --ran: 59 derived families, 58 run green, 1 NOT MEASURED (check:dual-build-cjs-loads, exit 3, needs a whole-tree build). pnpm lint green. After merging main (which brought fix(rest)!: an async import job whose every row failed ends failed, not succeeded #27795): @objectstack/rest tsc --noEmit green and full suite green (353 files, 7027 passed, 350 skipped); the derived gate list is unchanged.

Clause-②: no
Metadata-protocol: none

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cyUucJoh7384vix5rh

…workbook stream

The xlsx export opened its streaming workbook writer before the first
read, which committed the response to a streaming 200, so a query the
read refused (for example a search over a field the object does not
have) reached the client as a 200 instead of the 400 envelope csv and
json answer. Open the writer after the first page is read, before the
zero-row break, mirroring the json fix.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyUucJoh7384vix5rh
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json da94c11337fdb4c37ee809e9748a76791a5199cc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9734ad831fc052fe43ae14eab497789d601feab1 — the merge of head 733ab4dc94cfe89e33edc0615ddb8377b8ebe8a1 into base da94c11337fdb4c37ee809e9748a76791a5199cc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9734ad831fc052fe43ae14eab497789d601feab1 && git checkout 9734ad831fc052fe43ae14eab497789d601feab1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin da94c11337fdb4c37ee809e9748a76791a5199cc 733ab4dc94cfe89e33edc0615ddb8377b8ebe8a1 && git checkout -B drift-repro da94c11337fdb4c37ee809e9748a76791a5199cc && git merge --no-ff 733ab4dc94cfe89e33edc0615ddb8377b8ebe8a1

node scripts/docs-audit/affected-docs.mjs --json da94c11337fdb4c37ee809e9748a76791a5199cc

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

维护者速读

  • 改了什么:导出选 Excel(xlsx)时,先读到第一页数据再打开工作簿;查询被拒时回 400 说明错误,与 csv、json 一致。
  • 为什么改:之前工作簿一打开响应就定成 200,查询出错时用户拿到坏的或空的 Excel 文件(rest: GET /data/OBJECT/export?format=xlsx commits its workbook stream before the first read, so a refused query answers 200 instead of the 400 envelope (the xlsx half of #27682) #27755)。
  • 风险与回滚:源码只挪了一行(+7/-1);成功导出的状态码、响应头和单元格不变,零行导出仍是可打开的空工作簿。回滚即还原本 PR。
  • 先看:data-action-routes.ts 导出循环里第一次读完后打开 createXlsxStream 的那一行。
  • 两分钟验证:export-xlsx-refusal-envelope.test.ts,修复前 2 红 5 绿,修复后 7 绿。
  • 需要你做的:无(非受管、无需契约复审,席位按本任落地授权落地)。

席位意见:ACCEPT。

席位核对

Head-sha: 733ab4dc94cfe89e33edc0615ddb8377b8ebe8a1

  • Shape: the xlsx writer opens inside the read loop on the first chunk, after the first findData answers and before the zero-row break, beside json's [ (fix(rest): a refused json export answers the 400 envelope, not a truncated 200 stream #27753). Every later xlsx! read (rows, the zero-row header, finalize) runs only after the first chunk, so the writer is always open there. A refusal on a later page is not widened (triage 6510461172). Changeset patch @objectstack/rest, Clause-②: no, Metadata-protocol: none. check-governed-merges --pr 27888: NOT governed, 181 changed lines.
  • Premise measured: both refusal rows (400 INVALID_FIELD, 400 VALIDATION_ERROR) red on the unfixed code (expected 200 to be 400), green after; preservation rows (1 page, 3 pages, 0 rows) green before and after; @objectstack/rest 353 files / 7027 tests after the main merge (dev report 6513632867 on rest: GET /data/OBJECT/export?format=xlsx commits its workbook stream before the first read, so a refused query answers 200 instead of the 400 envelope (the xlsx half of #27682) #27755).
  • Gate check-runs on the head: 48 runs, 35 success, 13 skipped, 0 failure/pending; mergeable_state clean. check:dual-build-cjs-loads NOT MEASURED locally (needs whole-tree dist); CI builds cover it.
  • Contract review: not owed (no contract face: no packages/spec, no narrowing, no new public surface).
  • Accepted deviations: none beyond the report's two (card relation kept to the PR body only; no assignee/reviewer written by the thread).
  • Landing: under the maintainer's landing grant for this seat term (seat 4 thread, 2026-10-29 22:04Z): pr_ready + automerge_enable through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 03:04
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 03:04
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 2a3dec2 Oct 30, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-nc7szf branch October 30, 2026 03:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant