Skip to content

A raw TypeError: not a function leaks in the 400 envelope for {"title": 12345} — no code, no fields[] #7543

Description

@huangyiirene

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

  1. Boot showcase on a fresh isolated file DB (SqlDriver / better-sqlite3); authenticate as admin@objectos.ai.
  2. POST /api/v1/data/showcase_task with body {"title": 12345} (a number into the declared text field) → 400 {"error":"TypeError: not a function","object":"showcase_task"}. Repeats 3/3.
  3. Controls on the same route: body {} → 400 VALIDATION_FAILED with fields[]; a bad enum value → invalid_option with the allowed list.

Source

Extracted from the QA run #7463 (framework a86db17).

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Triage: domain:cli appended (kept pm:queue). The body's "label it once the throw site is found" condition is now met — the emitting seam is located, on origin/main @ 34c01a5:

    1. The throw: examples/app-showcase/src/data/hooks/index.ts:34 — hook source if (ctx.input.title) ctx.input.title = ctx.input.title.trim();. A numeric title (12345) is truthy, has no .trim ⇒ TypeError. This is why validateOne's safe String(value) never sees it — the hook throws first.
    2. The wire shape: packages/rest/src/rest-server.ts mapDataError, the sandbox-wrapper fallback (~:964-976): it strips the hook '<name>' threw: wrapper, deliberately keeps non-default error names (TypeError: …), and returns { status: 400, body: { error: msg, object } } — no code. That is byte-for-byte the observed response, and it answers the extraction note's "matches no branch of mapDataError" (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 one packages/rest branch, and fixing it covers every hook-thrown TypeError, not just this repro ⇒ principal landing packages/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.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 11, 2026
  3. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Claim — PM loop domain:cli, session session_0158ZQo7LiHSxGWpYKuPq1wu (os-help seat, #6024), wave 5.

    Unblocked: this was held behind #7525 because both touch the /api/v1/data error boundary. #7525 landed minutes ago as PR #7575 (d6f3f2fe5).

    ⚠️ That landing changes your starting point. #7575 rewrote mapDataError's explicit-status gate into a named declaredHttpStatus() reading status → statusCode over the 400–599 band. The card's tree read predates it. Re-measure on today's main before 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, an object key, no code — matches no branch of mapDataError. #5489 moved that mapper's terminal branch to a sanitised 500 INTERNAL_ERROR with no object key; the declared-4xx passthrough in resolveErrorResponse does not attach object; every remaining 400 branch carries a code. And validateOne in packages/objectql/src/validation/record-validator.ts handles a number in a text field safely via String(value). So the emitting seam is somewhere else, and finding it is step 1 — ⛔ do not assume mapDataError and start patching it.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions