Description
Symptom
POST /api/v1/data/showcase_task with a number in a declared text field:
{"title": 12345}answers:
400 { "error": "TypeError: not a function", "object": "showcase_task" }
— a raw JS runtime error as the client-facing message, with no code and no fields[]. Reproduced 3x.
Expected: the ledgered envelope the neighbouring invalid bodies produce on the same route in the same run:
| body | observed | |
|---|---|---|
{} |
400 VALIDATION_FAILED with fields[] |
correct |
| bad enum value | 400 with invalid_option and the allowed list |
correct |
{"title": 12345} |
400 "TypeError: not a function", no code |
defect |
Two separate contract breaks in one response: a handler-internal TypeError reaching the wire verbatim, and an error body outside the ledgered envelope shape (a client keying on code / fields[] gets neither).
Root cause
Not fully located in the report. Its suspicion: a coercion path calls a string method on a number before validation reports the type mismatch, so the write throws a TypeError instead of producing a field error.
Investigation note from extraction (working tree checked 2026-08-11, so verify against a86db175 before acting): the observed body shape — status 400, raw message, an object key, and no code — matches no branch of mapDataError in packages/rest/src/rest-server.ts on current main. #5489 moved that mapper's terminal branch to a sanitised 500 INTERNAL_ERROR (no object key), the declared-4xx passthrough in resolveErrorResponse does not attach object, and every remaining 400 branch carries a code. Likewise validateOne in packages/objectql/src/validation/record-validator.ts handles a number in a text field safely (String(value)), so the throw is upstream or downstream of it. Locating the emitting seam is step 1 of the fix.
No domain:* label applied deliberately: the fix could land in packages/rest (envelope classification ⇒ domain:cli) or in packages/objectql (the coercion that throws ⇒ domain:engine-core), and the extraction pass could not tell which from the report plus a tree read. Label it once the throw site is found.
Reproduction
- Boot showcase on a fresh isolated file DB (
SqlDriver/ better-sqlite3); authenticate asadmin@objectos.ai. POST /api/v1/data/showcase_taskwith body{"title": 12345}(a number into the declaredtextfield) →400 {"error":"TypeError: not a function","object":"showcase_task"}. Repeats 3/3.- Controls on the same route: body
{}→400 VALIDATION_FAILEDwithfields[]; a bad enum value →invalid_optionwith the allowed list.
Activity
os-zhuang commentedon Aug 11, 2026
Triage:
domain:cliappended (keptpm:queue). The body's "label it once the throw site is found" condition is now met — the emitting seam is located, onorigin/main@34c01a5:examples/app-showcase/src/data/hooks/index.ts:34— hook sourceif (ctx.input.title) ctx.input.title = ctx.input.title.trim();. A numerictitle(12345) is truthy, has no.trim⇒TypeError. This is whyvalidateOne's safeString(value)never sees it — the hook throws first.packages/rest/src/rest-server.tsmapDataError, the sandbox-wrapper fallback (~:964-976): it strips thehook '<name>' threw:wrapper, deliberately keeps non-default error names (TypeError: …), and returns{ status: 400, body: { error: msg, object } }— nocode. That is byte-for-byte the observed response, and it answers the extraction note's "matches no branch ofmapDataError" (this branch was missed; it matches).Both contract breaks the card names (raw runtime error verbatim + non-ledgered envelope without
code/fields[]) are produced by that onepackages/restbranch, and fixing it covers every hook-thrownTypeError, not just this repro ⇒ principal landingpackages/rest⇒domain:cli. The showcase hook's own type-unsafety (examples/**follows the subsystem it exercises) is a secondary hardening the dev can take in the same PR or leave.POST /api/v1/automation/:name/toggleanswers 500 INTERNAL_ERROR instead of 404 NOT_FOUND for an unknown flow #7535 toggle-500, SECURITY:DELETE /api/v1/reports/:idanswers 500 for another owner's report but 204 for a nonexistent id — an enumeration oracle over report ids #7523 reports-oracle) are different routes/mechanisms./meta/:type上一个未分类的服务端故障被报成 HTTP 400 —— handleRouteError 的兜底把 outage 说成客户端错误 #5489 (mapper terminal branch) is closed history the card already cites.target:v17: not applied — error-path envelope quality on invalid input; no data loss/security/migration face, controls on the same route answer correctly.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-help commentedon Aug 11, 2026
Claim — PM loop
domain:cli, sessionsession_0158ZQo7LiHSxGWpYKuPq1wu(os-help seat, #6024), wave 5.Unblocked: this was held behind #7525 because both touch the
/api/v1/dataerror boundary. #7525 landed minutes ago as PR #7575 (d6f3f2fe5).mapDataError's explicit-status gate into a nameddeclaredHttpStatus()readingstatus→statusCodeover the 400–599 band. The card's tree read predates it. Re-measure on today'smainbefore concluding anything — and note that the card's most useful observation may now read differently.The card's investigation note is the most valuable thing in it, and it is a negative result: the observed body —
400, raw message, anobjectkey, nocode— matches no branch ofmapDataError. #5489 moved that mapper's terminal branch to a sanitised500 INTERNAL_ERRORwith noobjectkey; the declared-4xx passthrough inresolveErrorResponsedoes not attachobject; every remaining 400 branch carries acode. AndvalidateOneinpackages/objectql/src/validation/record-validator.tshandles a number in atextfield safely viaString(value). So the emitting seam is somewhere else, and finding it is step 1 — ⛔ do not assumemapDataErrorand start patching it.claude/issue-7543-typeerror-in-400-envelopepackages/rest(envelope classification ⇒ this lane) or inpackages/objectql(the coercion that throws ⇒domain:engine-core, not this lane). The extraction pass deliberately could not tell which. If the throw site turns out to be inpackages/objectql, stop and report the located seam rather than fixing it — that is a re-routing decision for triage/PM, not a scope you take unilaterally. Locating it precisely is a complete and valuable deliverable on its own.TypeErrorreaching the wire verbatim, and a body outside the ledgered envelope (a client keying oncode/fields[]gets neither).{}→400 VALIDATION_FAILEDwithfields[], and a bad enum →invalid_optionwith the allowed list.{"title": 12345}must join them, not merely stop throwing.packages/rest/src/rest-server.ts— Three ledgered /meta routes are never mounted and die in the/meta/:typecatch-all — the route audit can't see this class because it treats the ledger as ground truth for what's mounted #7526 (/metaroute registration) and Public lookup route resolves the picker target from legacy field spellings only — omittedpublicPicker.objectanswers 500 against a canonical object schema #7486 (/forms/:slug/lookup/:field). The error-mapping region is now free (yours).claude-opus-5·mode:cloud.Generated by Claude Code
test(dogfood,runtime): reverse the E2E pin that kept a hook's `TypeEr…
fix(rest): a crashing hook body answers the sanitised fault envelope,…
fix(objectql): name the withheld read-only key when a hook faults rea…
docs(rest): re-anchor the dead tracker citations in packages/rest/src…