Repository navigation
Malformed JSON request body is silently parsed as {}: a truncated POST /data/:object/query answers 200 with the whole table, and a truncated PATCH answers 200 with an empty update #23914
Description
Filing gate ①: a product defect with a named landing and a reproduction. Reach: the public HTTP door POST /api/v1/data/:object/query, measured live twice by two independent agents on main 94114c1f.
Reader who acts: the REST/transport lane seat owning packages/plugins/plugin-hono-server and packages/adapters/hono. An unparseable JSON body must be refused with a 400 at the transport, never handed on as {}.
What
When a request declares Content-Type: application/json and the body is not valid JSON, the HTTP layer swallows the parse error and runs the handler with body = {}. On the query door this is the #3899 silent-widening class: a malformed filter answers 200 with the whole table instead of a 400. On the write door, a malformed PATCH answers 200 and performs an empty update.
Reproduction (fresh objectstack dev --seed-admin showcase boot, admin session)
Send each body from a file (curl -H 'Content-Type: application/json' --data-binary @b.txt …) so shell quoting plays no part.
POST /api/v1/data/showcase_account/querywith{"where": {"status": "churned"}}→ 200, 2 rows. Correct.- Same request with the closing brace missing:
{"where": {"status": "churned"}- expected: 400 (an invalid-JSON / validation code);
- actual: 200 with every row (14 of 14 on a fresh DB; 26 of 26 on a DB with more seed).
{ not json→ 200, every row.- A valid where-body sent as
text/plain→ 200, every row.parseBody()yields{}and the filter is dropped. PATCH /api/v1/data/showcase_account/IDwith{"name": "HACKED"(truncated)- expected: 400;
- actual: 200, with no field changed and
updated_atbumped. The client is told a write succeeded that never happened.
POST /api/v1/data/showcase_accountwith a truncated body containingname→ 400VALIDATION_FAILED"Account Name is required". The error is misleading: the body was discarded, and no parse error is named.
Mechanism
packages/plugins/plugin-hono-server/src/adapter.ts(~L691–710):let body = {}; onapplication/json,try { body = await c.req.json() } catch { try { body = await c.req.parseBody() } catch {} }. Both failures are swallowed.packages/adapters/hono/src/index.ts(~L322–329)readDispatchBody:try { return JSON.parse(await raw.clone().text()) } catch { return {} }. The same behaviour on the second transport.packages/runtime/src/domains/meta.ts(~L1314) documents ac.req.json().catch(() => ({}))catch-all.- No HTTP handler emits an invalid-JSON code; a grep for
INVALID_JSON/ "Malformed JSON" finds only CLI and driver code. A new code needs an ADR-0112 ledger row.
Contract it violates
docs/qa/platform-checklist/areas/api-backend.json → api-backend.query-contract-matrix (P0), negative: "silent WIDENING is the catastrophic failure mode (#3899): a malformed query body must never degrade into an unfiltered 200 full read". Item title: "malformed input 400s with the exact code".
Dedupe
MCP search_issues on objectstack-ai/objectstack, closed included:
malformed JSON request body silently treated as empty object instead of 400→ 4 hits (runtime: the dispatcher'sPATCH /data/:object/:idhas no request-schema gate, so a non-record body answers500with the engine's own error, where REST answers400 VALIDATION_FAILED#23491, Action-body writes have no not-found gate:ctx.api.object().update()against a nonexistent id answers 400 (or worse) instead of 404, while the protocol and callData paths both gate correctly #7867, better-auth update-user validation failure returns raw 500 empty body (should be a 4xx) #3398, A rawTypeError: not a functionleaks in the 400 envelope for{"title": 12345}— nocode, nofields[]#7543). runtime: the dispatcher'sPATCH /data/:object/:idhas no request-schema gate, so a non-record body answers500with the engine's own error, where REST answers400 VALIDATION_FAILED#23491 is a PATCH non-record body 500 on the dispatcher, a different bug.invalid JSON body parseBody fallback hono adapter swallow→ 2 hits (hono adapter:createHonoApp's catch-all renders an error thrown out ofdispatch()fromstatusCodealone, so a declaredstatus/code(a missing record's404 RECORD_NOT_FOUND, an outage's503) answers500 INTERNAL_ERROR#23365, hono adapter: render an escaped throw that carries a declared ADR-0112status+ registeredcodeas that envelope, not as bare500 INTERNAL_ERROR "No response from handler"— #15999's shared half #16545; both about error-envelope rendering).unparseable request body becomes {} query widens returns all rows→ 0 hits.INVALID_JSON body syntax error 400→ 1 hit (better-auth update-user validation failure returns raw 500 empty body (should be a 4xx) #3398, unrelated).- The first runner's three queries → 10 hits, no match.
No existing card covers this.
QA-source: #23913 · api-backend.query-contract-matrix · clause 6 (silent-widening negative)
Activity
objectstack-fleet commentedon Oct 16, 2026
Triage: first grade,
bug·priority:p1·target:v18·domain:cli·area:api·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T12:48ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/maind5666be876:packages/plugins/plugin-hono-server/src/adapter.ts:691openslet body: any = {};before the swallowedc.req.json()/parseBody()pair. The filed reading holds.domain:cli:plugin-hono-serverandpackages/adapters/*aredomain:cliby the domain table.target:v18: refusing an unparseable body with a 400 narrows the REST / data door, which the maintainer's protocol-first rule makes p1 +target:v18in every lane. The measured faces are also wrong answers at a public door:PATCHanswers 200 for a write that never happened.plugin-hono-server(packages/adapters/hono, and any sibling with the samecatch {}). Steps 4 (text/plain) and 6 (the misleadingVALIDATION_FAILED) are part of the done line.objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 1 (#23914: a request whose body cannot be parsed is refused with a 400 at the transport, never handed on as
{}. A truncatedPOST /data/:object/querystops answering 200 with the whole table, and a truncatedPATCHstops answering 200 for a write that never happened) · 2026-10-16T15:45ZSession:
session_018t6a5rvqPRvEDMCVVW9kYTAccount:
os-tesla(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-23914-malformed-body-refusalWorktree:
objectstack-issue-23914Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)Scope:
batch3, counting dev runs. In flight before this act: #23050 slice 10 (6218443248) and #23181 slice 4 (6218822258). #15953 slice 7 (PR #23976) and #23858 (PR #23979) are in landing.Order, read at this act: this card is the lane's only open, unclaimed p1 (
target:v18, graded 2026-10-16T12:48Z). #23948 (p0) and the other p1 cards are seat 1's orhotlong's.File surface (read on
origin/main2054a0888c; stop on a breach and explain it in the report):packages/plugins/plugin-hono-server/src/adapter.ts, the body parse only (about:691–:712, thec.req.json()/parseBody()pair whose failures are swallowed).packages/adapters/hono/src/index.ts,readDispatchBodyonly (about:322–:329, its two{}fallbacks).packages/runtime/src/domains/meta.ts, thec.req.json().catch(() => ({}))catch-all (about:1314), only if measurement shows a request reaches it with a declared JSON body..json().catch(,catch {}around a body parse andreturn {}from a body reader, and classifies each. In scope: a door that takes a client body. Out: a reader of a body this platform wrote itself, with the reason.ERROR_CODE_LEDGERcode.INVALID_REQUESTis on the ledger. ⛔ No new code: a new code isClause-②: yesand needs a ledger row. If no existing code fits, stop and report it asneeds_decision.application/jsonbody that does not parse is refused.text/plain, and step 6, the misleadingVALIDATION_FAILEDon a discarded body, are in the done line, as triage requires.{}legitimately.{ not json,text/plain, truncatedPATCH, truncated create), each refused 400 naming the parse failure. CONTROL: a valid body answers as before on each door, and so does an empty body where it is legitimate..changeset/23914-*.md(patchor as the narrowing rule requires, for each package whose shipped behaviour changes), carrying theClause-②line below.packages/rest/src/rest-server.ts([maintainer] refactor(rest): splitpackages/rest/src/rest-server.ts(13,701 lines, 70 commits in 30 days) along itsregister*Endpointsseams, one move-only slice per PR,/metaafter #22877 stage 2 #23050 slice 10 is moving its UI section).Container & model:
M(one refusal across two transports and their siblings, a narrowing of a published door, measured at the door),mode:subagent,model: TIER_DEFAULTfor the build. The narrowing face owes aCONTRACT_REVIEW_TIERreview before the queue (below).Clause-②: no (narrowing)
Basis for the Clause-② line:
{}. After this card they refuse it with a 400. That narrows the accepted inputs of a published door, as triage's grade says ("refusing an unparseable body with a 400 narrows the REST / data door").INVALID_REQUESTis already on the ledger. Nopackages/specedit.references/contract-review.md, a(narrowing)declaration owes oneCONTRACT_REVIEW_TIERreview, served by this seat before the queue.Responsibility:
the HTTP transports in this lane (plugin-hono-server, adapters/hono) and the runtime's meta domain | the api-backend checklist item api-backend.query-contract-matrix (P0): "a malformed query body must never degrade into an unfiltered 200 full read"; #3899's silent-widening class | every client whose request body is cut short or mis-typed: a query reads the whole table and a write reports success it did not makeThread-read: 6215847310
Serial constraints cleared: every open PR's file list was read at this act; none touches the three files above. Seat 1's os migrate organization-ownership never certifies a database whose packages declare sharing rules: it waits on the declared-rule-mirrors departure (D2) while the same release's boot still seeds them #23948 holds
packages/cli/src/utils/organization-ownership-*and its command, which are disjoint. Nopm:dispatchedclaim namesplugin-hono-server,adapters/honoorruntime/src/domains/meta.ts.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{ "issue": 23914, "status": "done", "branch": "claude/issue-23914-malformed-body-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/24005", "session": "session_018t6a5rvqPRvEDMCVVW9kYT", "premise_still_valid": true, "summary": "A request body an HTTP door cannot parse is now refused 400 INVALID_REQUEST (an existing ERROR_CODE_LEDGER code, no new row), never handed on as {}: plugin-hono-server refuses when a handler reads req.body (malformed declared JSON, malformed form, or a non-empty body in a media type it does not read, so the text/plain JSON of step 4 too), createHonoApp refuses a malformed declared-JSON body before dispatch() on both arms, and cloud-connection's four own routes refuse the same way. Measured at a fresh objectstack dev showcase door, base 2054a0888c vs final head b9df5b1d3: steps 2-4 went from 200 with 14 of 14 rows to 400; step 5 from 200 with updated_at bumped to 400 with the record untouched; step 6 from 400 VALIDATION_FAILED 'Account Name is required' to the parse refusal; every CONTROL (valid body, empty body, no body, valid PATCH/create, raw upload of a .txt as text/plain and of a JSON-with-comments file as application/json) answers as before. PM mechanism assumption 3 was falsified and moved the design: the storage presign tells clients to PUT a .json file as application/json and the raw-PUT route reads rawBody(), so plugin-hono-server raises the refusal when req.body is READ (stopping the handler there; the transport's 400 is the answer whatever the handler made of the throw), not before the handler runs. Three minor changesets with 'Clause-②: no (narrowing)' and an ADR-0087 not-required (no-migration-prescription) disposition; the seat's CONTRACT_REVIEW_TIER review is owed before the queue.", "tests": "All under scripts/pm/os-verify-lock.sh, exit codes captured before any pipe. FINAL HEAD b9df5b1d3: pnpm --filter @objectstack/plugin-hono-server exec vitest run --maxWorkers=2 :: exit 0 (30 files, 386 passed); pnpm --filter @objectstack/hono exec vitest run --maxWorkers=2 :: exit 0 (9 files, 176 passed); pnpm --filter @objectstack/cloud-connection exec vitest run --maxWorkers=2 :: exit 0 (46 files, 561 passed); pnpm --filter X typecheck :: exit 0 for all three (plugin-hono-server incl. check:test-typecheck OK; hono and cloud-connection tsc --noEmit + -p tsconfig.test.json). Post-merge refresh: pnpm install --frozen-lockfile :: exit 0; pnpm turbo run build --filter='!@objectstack/docs' --concurrency=2 :: exit 0 (75 tasks); pnpm --filter @objectstack/spec check:generated :: exit 0. New pins: plugin-hono-server/src/request-body-refusal.test.ts (20: card bodies refused with envelope parsed against ApiErrorSchema/envelopeViolations, handler never acts, no echo, handler's own 500 catch loses to the refusal, fallback seam both ways, real-socket step 4; CONTROL valid/empty/no-body/form/valid PATCH/3 byte doors stored byte for byte), adapters/hono/src/hono-body-refusal.test.ts (15: refusals with dispatch() never called, /auth/* fallback, CONTROL, cross-door parity HonoHttpServer vs createHonoApp equal status+body), cloud-connection error-envelope.conformance (4 refusals + empty-body CONTROL) and install-modes (refusal with zero writes + CONTROL); fixture triage: hono-catch-all-raw-request.test.ts's ['invalid JSON','application/json','{ not json',{}] row pinned the removed branch and was replaced. DOOR (objectstack dev --fresh showcase, curl --data-binary from files): base 2054a0888c step2/3/4 200 rows=14, step5 200 updated_at 16:06:38.737Z -> 16:07:18.807Z name unchanged, step6 400 VALIDATION_FAILED; head 0660b5a16 and final b9df5b1d3 step2/3/4/5/6 400 INVALID_REQUEST 'The request body could not be parsed as JSON (it was sent as application/json)...' / 'The request body was sent as text/plain, which this server does not read into a request body...', step5 updated_at unchanged; CONTROLs identical on both; also b9df5b1d3: cloud-connection/bind/poll truncated 400 parse refusal, {} 400 'device_code is required', marketplace/install-local truncated 400 parse refusal; 0660b5a16: PUT /meta/object/showcase_account and POST /keys truncated 400 parse refusal. ABLATION (scripts/ablation-replace.mjs WRAP, anchor x1 -> x0, subjects resolve from src so no rebuild): plugin-hono-server accessor refusal -> return {} (blob a331a0f6ed81 -> dee7fcca5045): 10 failed (every refusal, fallback-reads, socket step 4) / 10 passed (every CONTROL); hono declared-JSON throw deleted (b3d7844bbd26 -> 0a88eaa0cb2f): 9 failed / 6 passed; cloud-connection parse refusal -> {ok:true, body:{}} on 2d952cae1 (61561f4f3e17 -> 7c27524a66d0): 4 failed / 41 passed. Restore proven each run: blob == HEAD blob, git diff HEAD 0 bytes, git status --porcelain 0 lines, marker count 0.", "gates": "LOCAL on b9df5b1d3: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (merge base adce19297, no stale-tree warning) derived 66 families; all 66 run, every one :: exit 0 (incl. pnpm check:route-envelope, check:dispatcher-error-vocabulary OK, check:error-status-conformance, check:nul-bytes OK, check:published-files, check:dts-closure, check:test-source-alias, check:type-check-coverage, check:adr-0087-registration '3 declared-breaking changeset(s), each carrying an ADR-0087 disposition', check:empty-changeset, check-changeset-no-major); --ran reconciliation with 'COMMAND :: exit CODE' records :: exit 0, '66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN', all 66 carrying an exit code. pnpm exec eslint --no-inline-config over the 11 changed .ts files :: exit 0, no findings. First union on 0660b5a16 had check:route-envelope :: exit 1 (new cloud-connection helper built a response; the gate wanted a PLUGIN_ROUTE_MODULES row in scripts/check-route-envelope.mjs, outside the file surface) -> helper refactored to return a message, each declared route module builds its own 400 body -> exit 0. CI (PR #24005 head b9df5b1d3, read once after open, not waited on): 23 check-runs; 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped, all others in_progress: Check Changeset, Lint · ESLint, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Type Check · workspace, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Governed Surface Queue Guard, Check PR Size, Check Documentation Links, Flag docs affected by code changes, The card this PR closes must claim this branch, Part-of PR must not also close its card, No other open PR may claim the same single-writer path, filter. NOT MEASURED: Lint & Repo Gates / Test Core / Build Core aggregate contexts, reason: not yet reported at the read.", "mcp_calls": "0 — no MCP GitHub tool used (reads were gh api GETs: issue 23914 + comments, pulls/24005, its check-runs)", "api_writes": "4 REST writes via the fleet-write relay as objectstack-fleet[bot], in 2 dispatch strokes (POST /repos/objectstack-ai/objectstack/dispatches x2): stroke 1 pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft forced) + POST /repos/objectstack-ai/objectstack/issues/24005/assignees ['os-tesla']; stroke 2 = POST /repos/objectstack-ai/objectstack/issues/23914/comments (this report). label-write: none (per dispatch). git push (not REST) for the branch.", "deviations": [ "PM mechanism assumption 3 FALSIFIED and the route changed per the ruling's intent: the storage raw-PUT door relies on the transport not refusing (its presign answers headers content-type application/json for a .json file; objectui UploadProvider PUTs Content-Type: mimeType; dogfood suites PUT text/plain). plugin-hono-server therefore raises the 400 when a handler READS req.body, not before the handler runs; still the transport's 400 with an existing code, no handler change. createHonoApp refuses eagerly (no byte door there; forwarding domains read form/own bytes) but only declared JSON.", "Assumption 1 measured: the objectstack dev door is plugin-hono-server (HonoServer); createHonoApp is the embed door, mounted by nothing in packages/cli; both carried the swallow, both changed. Assumption 2 decided by the documented contract (content/docs/api/wire-format.mdx: Content-Type application/json required): text/plain is refused on plugin-hono-server, not parsed; createHonoApp's pre-existing content-type-blind parse is left unchanged (Acceptance note) — no needs_decision.", "File surface: beyond the two transports, the in-lane siblings under the claim's sibling clause — cloud-connection-plugin.ts (/bind/start, /bind/poll, /install), marketplace-install-local-plugin.ts (handleInstall), new module-private cloud-connection/src/request-json-body.ts, plus tests. runtime/src/domains/meta.ts untouched (its :1314 is a comment, not a body reader). scripts/check-route-envelope.mjs NOT edited: the gate's red was resolved inside the surface by refactoring the helper.", "Merged origin/main (adce19297, 9 commits, none touching the three packages) before opening the PR per AGENTS.md §10, with the §9 refresh; door run, suites, typechecks and the gate union re-taken on the final head b9df5b1d3.", "Changesets are minor (launch-window convention for a breaking narrowing), not patch; three files, one per package.", "Attribution: commit trailers are AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By (pre-push refuses a model identifier); the PR body ends with AGENTS.md's single session-URL footer instead of the harness's two lines.", "test:repo (vitest repo project) not run locally: the diff touches no repo-project test and the derivation does not name it; CI Test Core owns it. packages/cli integration tier: not applicable (no cli change).", "The refusal message lives in three module-private copies (plugin-hono-server, hono, cloud-connection): a shared export would enlarge a package's public surface, which this card's Clause-② no (narrowing) does not declare; the hono parity test holds the two transports equal." ], "files_changed": [ ".changeset/23914-cloud-connection-body-refusal.md", ".changeset/23914-hono-body-refusal.md", ".changeset/23914-hono-server-body-refusal.md", "packages/adapters/hono/src/hono-body-refusal.test.ts", "packages/adapters/hono/src/hono-catch-all-raw-request.test.ts", "packages/adapters/hono/src/index.ts", "packages/cloud-connection/src/cloud-connection-plugin.ts", "packages/cloud-connection/src/error-envelope.conformance.test.ts", "packages/cloud-connection/src/marketplace-install-local-install-modes.test.ts", "packages/cloud-connection/src/marketplace-install-local-plugin.ts", "packages/cloud-connection/src/request-json-body.ts", "packages/plugins/plugin-hono-server/src/adapter.ts", "packages/plugins/plugin-hono-server/src/request-body-refusal.test.ts", "packages/plugins/plugin-hono-server/src/request-body.ts" ], "open_questions": [], "out_of_scope_findings": [ "class: a · reach: POST /api/v1/auth/admin/unlock-user with the truncated body {\"userId\": \"usr_nobody\" sent as application/json answers 400 INVALID_REQUEST 'userId is required' (measured at a fresh objectstack dev showcase boot, admin session, on 0660b5a16; plugin-auth unchanged at b9df5b1d3) · evidence: plugin-auth's own routes keep the swallow this card removed elsewhere — try { body = await c.req.json() } catch { body = {} } at auth-plugin.ts:2735 (admin/oauth2/toggle-disabled), :2870 (admin/unlock-user), :3423 (sys-oauth-application/register), admin-import-users.ts:493, register-sso-provider.ts:250/:444/:501 — the step-6 shape (a misleading required-field refusal for a body that carried the field), not widening; lane is plugin-auth, not domain:cli · same family as #23914: name it for that family's closing card, not a point card · dedupe words: plugin-auth malformed body; c.req.json catch body {}; userId is required truncated body; unparseable request body auth routes", "carrier: none (承接者:无) · createHonoApp reads every content type as JSON: a JSON body sent as text/plain is parsed and a non-JSON body that does not declare JSON is {}; refusing it needs the dispatcher to know which domains forward the raw request (the /approvals/act form, inbound hooks) · noted in PR Acceptance notes, not filed", "carrier: none (承接者:无) · @objectstack/http-conformance NodeHttpServer (private, ships nothing) keeps catch { body = {} } · noted in PR Acceptance notes, not filed", "carrier: none (承接者:无) · runtime/src/domains/meta.ts:1314's comment still quotes the catch-all's pre-#22576 line c.req.json().catch(() => ({})); its subject (falsy JSON values arrive falsy) still holds · noted in PR Acceptance notes, not filed" ], "cleanup": "dev servers on 38914/38915/38916 started and stopped by recorded PID (ports free); no background job or monitor left running; after this comment's read-back the worktree's node_modules and the worktree ../objectstack-issue-23914 are removed (branch fully pushed at b9df5b1d3, tree clean)." }Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{ "issue": 23914, "status": "done", "branch": "claude/issue-23914-malformed-body-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/24005", "session": "session_018t6a5rvqPRvEDMCVVW9kYT", "addendum": "fix round for PR #24005's Test Core (3/6) red, with seat ruling B (contract review 6221858698) applied; fields not listed are unchanged from comment 6221346264", "head": "97d881a2e (pushed; plain pushes on top of b9df5b1d3, no rebase, no force, no merge: merge-clean against origin/main)", "files_changed": [ ".changeset/23914-cloud-connection-body-refusal.md", ".changeset/23914-hono-body-refusal.md", ".changeset/23914-hono-server-body-refusal.md", "packages/adapters/hono/src/hono-body-refusal.test.ts", "packages/adapters/hono/src/hono-catch-all-raw-request.test.ts", "packages/adapters/hono/src/index.ts", "packages/cloud-connection/src/cloud-connection-plugin.ts", "packages/cloud-connection/src/error-envelope.conformance.test.ts", "packages/cloud-connection/src/marketplace-install-local-install-modes.test.ts", "packages/cloud-connection/src/marketplace-install-local-plugin.ts", "packages/cloud-connection/src/request-json-body.ts", "packages/plugins/plugin-hono-server/src/adapter.ts", "packages/plugins/plugin-hono-server/src/request-body-refusal.test.ts", "packages/plugins/plugin-hono-server/src/request-body.ts", "packages/qa/http-conformance/src/hono-body-refusal.conformance.test.ts" ], "tests": "REPRO at b9df5b1d3 (closure built, under the lock): pnpm --filter @objectstack/http-conformance exec vitest run --maxWorkers=2 :: exit 1, 1 failed | 143 passed; file src/hono-webhooks-redeliver.conformance.test.ts, case 'status, code, headers and body bytes equal at both doors (the row id masked)', first assertion line 273, row 'not JSON': hosted body {\"success\":false,\"error\":{\"code\":\"INVALID_REQUEST\",\"message\":\"The request body could not be parsed as JSON (it was sent as application/json)...\"}} vs self-hosted mount {\"success\":false,\"error\":{\"code\":\"INVALID_REQUEST\",\"message\":\"Request body must be JSON.\"}}. Same case at merge base adce19297 (detached compare worktree): the file :: exit 0, 7 passed. ROOT CAUSE: createHonoApp refused before dispatch(), pre-empting the forwarding /webhooks/redeliver member (which reads its own bytes after its own sign-in check). FIX (ruling B): createHonoApp hands dispatch() a body whose every read throws RequestBodyRefusal and records the read; both arms answer the refusal only if it was read. The row stays as pinned (an interim local re-pin was reverted before any commit, blob == HEAD proven). RUNS on 0cee7c3d9 (= the code of 97d881a2e; its last commit only rewrites the hono changeset): pnpm --filter @objectstack/hono exec vitest run --maxWorkers=2 :: exit 0 (9 files, 180 passed); pnpm --filter @objectstack/http-conformance exec vitest run --maxWorkers=2 :: exit 0 (14 files, 150 passed; the redeliver row green as pinned); pnpm --filter @objectstack/hono typecheck :: exit 0; pnpm --filter @objectstack/http-conformance typecheck :: exit 0 (check:test-typecheck OK). BASE adce19297 full suite: pnpm --filter @objectstack/http-conformance exec vitest run --maxWorkers=2 :: exit 0 (13 files, 144 passed). plugin-hono-server and cloud-connection are byte-identical to b9df5b1d3: their suites there stand (386 / 561 passed). NEW PINS: packages/qa/http-conformance/src/hono-body-refusal.conformance.test.ts (6, REAL dispatcher + data domain through createHonoApp: steps 2/3/5/6 refused with the store read back unchanged, byte parity with a HonoHttpServer route, ORDER: an active ADR-0069 gate whose session read fails answers 503 not 400, CONTROL valid query filters + empty body is the empty query); adapters/hono/src/hono-body-refusal.test.ts rewritten to 18 (READER fixture domain answering its own 500 loses to the refusal, a read escaping dispatch(), /auth/* incl. DELETE, FORWARDER answers its own refusal with every byte, CONTROL, parity); hono-catch-all-raw-request.test.ts +1 (a declared-JSON body that does not parse reaches a forwarding domain unread). ABLATION on 0cee7c3d9 (scripts/ablation-replace.mjs WRAP, anchor '? refusedOnRead(new RequestBodyRefusal(MALFORMED_JSON_BODY_MESSAGE))' x1 -> x0, subjects from src): leg SWALLOW -> readable({}) (blob fb199e0ebefb -> 9c5714c12488): hono 10 failed / 8 passed, http-conformance 4 failed / 9 passed (order case, CONTROL and every redeliver row green); leg REFUSE-BEFORE-DISPATCH (the b9df5b1d3 shape) -> immediate throw (fb199e0ebefb -> 4f27b2629aad): hono 7 failed / 11 passed (reader-ownership cases, /auth/*, forwarder), http-conformance 2 failed / 11 passed (the redeliver byte-parity row and the gate-order case). Restore: blob == HEAD fb199e0ebefb, git diff HEAD 0 bytes, git status --porcelain 0 lines, marker count 0.", "gates": "LOCAL on 97d881a2e: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (merge base adce19297) derived 66; all 66 run, every one :: exit 0; --ran with 'COMMAND :: exit CODE' records :: exit 0, '66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN'. Stale-input flag: scripts/check-route-envelope.mjs changed on main after the base (d132140eb, #23050 slice 10: moves declared counts rest-server.ts -> ui-routes.ts; this diff touches neither); branch merge-clean (git merge-tree exit 0), so not merged per the round's instruction. Added to the list: pnpm --filter @objectstack/http-conformance test — base adce19297 :: exit 0 (144 passed), b9df5b1d3 :: exit 1 (143 passed, 1 failed), head :: exit 0 (150 passed). pnpm turbo run build --filter='!@objectstack/docs' --concurrency=2 on 97d881a2e :: exit 0 (75 tasks). pnpm exec eslint --no-inline-config over the 12 changed .ts files :: exit 0, no findings. CI on 97d881a2e not read: NOT MEASURED, reason: pushed at report time, not waited on.", "deviations": [ "Seat ruling B applied as the mechanism; no new export from any package, no route or prefix list in the adapter; packages/runtime untouched (dispatch() never reads body, so no runtime change was needed). The not-JSON redeliver row is unchanged.", "Changeset .changeset/23914-hono-body-refusal.md rewritten: refused when the dispatched domain reads it; '/auth/* fallback hands a body to every method but GET and HEAD' (DELETE included); 'the catch-all dispatches GET, HEAD and DELETE with no body'; 'the request handed on beside the body still arrives unread … a domain that forwards it … answers exactly as before, a malformed JSON body included'. check:adr-0087-registration green in the union.", "PR #24005's body is NOT updated by me: .claude/agents/os-dev.md allows one PR-body write (at pr_create) and no PATCH, so the seat applies it. Full corrected body (v2, 19830 bytes, no tag-shaped fragments, single closing line 'Fixes #23914') at /tmp/claude-0/-home-user/ab77ba73-22e7-5d29-a840-c87e921818a0/scratchpad/issue-23914/pr-body-v2.md. It replaces the hono row and the eager paragraph with a ruling-B section, corrects the false NodeHttpServer note (the suites DO drive a malformed body, through the two Hono doors, not through NodeHttpServer) and the Ordering note, and refreshes Tests/Ablation/Gates to 97d881a2e.", "Disk ran out mid-round (ENOSPC, 54M free): another session's scratchpad, scratchpad/issue-23805 (21G, last written 08:48; emitted-run and fixed-run 8.4G each), held it. I did not touch it; I removed only my own compare worktree. It was freed by its owner later (22G free).", "The base run used a detached compare worktree ../objectstack-issue-23914-cmp at adce19297 (no branch), removed after the run (clean, no --force)." ], "mcp_calls": "0", "api_writes": "this round: 1 REST write via the fleet-write relay as objectstack-fleet[bot] (POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/23914/comments, this addendum); git push of the branch; nothing on PR #24005", "open_questions": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
ACCEPTed: PR #24005 at
97d881a2e7·domain:cliseat 2 ·os-tesla· sessionsession_018t6a5rvqPRvEDMCVVW9kYT· 2026-10-16T19:22Z · holder of claim6219157652.6222992052on PR fix(plugin-hono-server,hono,cloud-connection)!: refuse an unparseable request body with 400 INVALID_REQUEST instead of reading it as {} #24005. Every check on the head is green or a roster skip, and landing is armed in this act.CONTRACT_REVIEW_TIER:6221858698was FAIL onb9df5b1d30, on the redTest Core (3/6). The seat ruled B: the body's reader refuses it on both transports.6222965551is PASS on this head.plugin-auth's own body-parse swallow (that lane's family card);createHonoApp's parse of undeclared bodies;IHttpRequest.body's stale TSDoc (domain:spec).Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Landed: PR #24005 →
75ced061b8(#23914,Fixes) ·domain:cliseat 2 ·os-tesla· sessionsession_018t6a5rvqPRvEDMCVVW9kYT· 2026-10-16T19:53Zmerged: true, merge commit75ced061b8, merged 2026-10-16T19:53Z through the merge queue (enqueued 2026-10-16T19:25Z)origin/main75ced061b8is an ancestor oforigin/main: 15 files, +1276 / −51, as reviewed at97d881a2e7gh-readonly-queue/main/*pr-24005*head remains## Contract review6221858698(FAIL onb9df5b1d30), seat ruling B (the body's reader refuses it),## Contract review6222965551(PASS on this head), ACCEPT6222992052, card pointer6223012008.400 INVALID_REQUESTand never handed on as{}. That holds onplugin-hono-server(whenreq.bodyis read), oncreateHonoApp(when the dispatched domain reads the body) and oncloud-connection's four routes. Forwarding domains read their own bytes as before.plugin-auth's seven routes,domain:services) is filed.createHonoApp's parse of undeclared bodies andIHttpRequest.body's stale TSDoc (domain:spec) go in this seat's next filing window.pm:dispatchedis removed in the stroke after this comment.Generated by Claude Code