Repository navigation
qa/http-conformance: the reference IHttpServer adapter's body diverges from the IHttpRequest.body contract on three counts — malformed JSON becomes {}, a multipart body becomes {}, and a text/* body becomes a string #24083
Description
Filing gate ①: a published contract sentence that a named producer contradicts. reach: packages/qa/http-conformance/src/adapter.ts, the node:http IHttpServer the conformance suite runs as the reference implementation, the suite that locks cross-adapter behaviour.
Filed by domain:spec seat 1 (seat post #6017) · os-elon-musk · session session_01HjR8RUwTPQ3TgbfKihmxp9, from the contract review of PR #24068 (#24043, 6226498420, escalation 1) and that PR's dev report (6226147853, out-of-scope finding). ⛔ Not a claim. Grade and lane are triage's; the filer's guess is domain:cli · area:api, the lane of #23914, #24005 and #24042.
The contract (since PR #24068, merged 8a83cebc15)
packages/spec/src/contracts/http-server.ts IHttpRequest.body:
- the body is parsed by its declared content type: JSON for
application/json, form fields formultipart/form-dataandapplication/x-www-form-urlencoded; - it is
{}when the request carries no body; - reading it may refuse: an implementation that cannot parse the body, or does not read its media type, throws from the read rather than hand over an invented value;
- the standard transport (
@objectstack/plugin-hono-server, PR fix(plugin-hono-server,hono,cloud-connection)!: refuse an unparseable request body with 400 INVALID_REQUEST instead of reading it as {} #24005) does refuse, and answers 400INVALID_REQUEST.
Where the reference adapter differs (origin/main)
packages/qa/http-conformance/src/adapter.ts:
- Malformed declared JSON becomes
{}(about :387):try { body = text ? JSON.parse(text) : {}; } catch { body = {}; }. This is the pre-fix(plugin-hono-server,hono,cloud-connection)!: refuse an unparseable request body with 400 INVALID_REQUEST instead of reading it as {} #24005 invented-{}shape that 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 removed from the standard transport. - A
multipart/form-databody with content becomes{}(isEagerlyParsedabout :102-:107 leaves multipart out, andbodykeeps its{}default at about :381). It is neither parsed into fields, as the contract says, nor refused. - A
text/*body becomes a string (about :391). The contract names no string value forbody. The standard transport refuses a non-empty body in a media type it does not read.
No conformance test drives this adapter with a malformed body. The only body-refusal suite, plugin-hono-server/src/request-body-refusal.test.ts, drives the two Hono doors. So the suite that exists to catch a cross-adapter difference cannot see this one.
PR #24005's contract review (6222965551) accepted count 1 "for the private harness" while the contract said nothing about body. Since PR #24068, the contract does say what body is.
Done when
- The reference adapter's
bodyfollows the contract on all three counts: it refuses (or parses, for multipart fields) where the standard transport does, and never invents{}for a body it could not read. Either match the standard transport'sreadRequestBodybehaviour, or reuse it, whichever the lane judges right. - A conformance case drives both adapters with a malformed declared-JSON body and a non-empty body in an unread media type, and pins one answer for both.
- ⛔ No change to the
IHttpRequesttype.
Rider (wording, spec lane, optional): the new TSDoc's raw-bytes sentence says "(a file upload, a media type not listed above)". The review notes that "a file upload" invites the multipart reading; "a raw upload (a PUT of the file's own media type)" is the case the transport's module note names. If the lane that takes this card touches the contract, it can fix that phrase too; otherwise the spec lane will.
Not filed, noted for whoever measures it: the review read, but did not run, hono 4.13.12's body cache. After the eager parseBody(), the standard transport's rawBody() on a multipart request would return a re-serialised form with a fresh boundary, while rawBody's TSDoc says "multipart parts". Unmeasured and unpinned either way. Dedupe words: multipart rawBody re-serialised boundary; hono cachedBody rawBody after parseBody.
Dedupe
MCP search_issues on this repository: http-conformance adapter malformed JSON body {} conformance body refusal parity (4 hits) and NodeHttpServer conformance adapter request body multipart text parsing diverges from hono (4 hits). The nearest are #24042 (createHonoApp's non-JSON bodies, domain:cli), #24032 (plugin-auth's raw-Hono routes) and the closed #23914. Neither open card, nor any comment on them, names packages/qa/http-conformance.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 17, 2026
Triage: first grade,
priority:p3·domain:cli·area:api·tests·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-17T01:53ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
@objectstack/http-conformanceis"private": trueand calls itself "validation instrument, not a product server". The cost is a blind conformance case, the same class as a fixture that hides a defect.domain:cli:packages/qaisdomain:cliby the lane table. The filer's guess holds.origin/main50ccfa6697:packages/qa/http-conformance/src/adapter.ts:387still readstry { body = text ? JSON.parse(text) : {}; } catch { body = {}; }.IHttpRequest. The rider (the TSDoc phrase) rides only if the lane touches the contract.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 1, serial, S-size cards only, any priority (the maintainer to this seat, verbatim 「用中文,当前任务处理完,后续只派小卡」, confirmed as "S-size, any priority"). #24150's PR #24231 waits only in the merge queue (
enqueued, read in this act). Label order read in this act: this lane's queued p1/p2 cards hold no S-size card for this seat (#23050's 13b–13e are L/M and 13b is seat 2's); among the p3 cards, #24151 (20 sites), #23217 (five doors and a newcoreexport) and #15953 (an epic's residue) are not S-size, and #24130 and #24091 are claimed by seat 2 (6243651386,6242873833). #24083 is the first S-size p3. #24083: the referenceIHttpServeradapter'sbodyfollows theIHttpRequest.bodycontract on all three counts, and one conformance case pins the same answer from both adapters · 2026-10-18T00:34ZSession:
session_01BVm1pAXVXQ3PjC42j1kPFfAccount:
os-sales(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24083-conformance-adapter-bodyWorktree:
objectstack-issue-24083Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)File surface (read on
origin/main2bcd406a2; line numbers move; stop on a breach and explain it in the report):packages/qa/http-conformance/src/adapter.ts:isEagerlyParsed, about:102-:107; the body read, about:375-:392);bodytheIHttpRequestliteral carries (about:403);One new conformance case under
packages/qa/http-conformance/src/. It drives both adapters with:It pins one answer for both adapters, and an empty body as the
{}control.packages/qa/http-conformance/src/adapter.test.ts, only where an existing assert pins one of the three old answers.At most one
.changeset/24083-*.md, asCheck Changesetreads this private package.⛔ Hold-offs:
packages/spec/**:IHttpRequestdoes not change. The card's TSDoc rider is the spec lane's and does not ride here.packages/plugins/plugin-hono-server/**,request-body.tsincluded. It is the subject the reference is measured against, so it is mirrored, not imported.adapter.tsabout:203-:208, pinned byfallback-seam.conformance.test.ts).rawBody()cache, which the card notes and does not file.Container & model:
S(one source file in a private validation instrument, one conformance case),mode:subagent,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate)Clause-②: no
Basis for the Clause-② line:
@objectstack/http-conformanceis"private": trueand calls itself a validation instrument, not a product server, so no published accept set moves.The published contract this follows,
IHttpRequest.body(PR docs(spec): IHttpRequest.body TSDoc states what a handler reads and that the read may refuse #24068), and the standard transport that already honours it (PR fix(plugin-hono-server,hono,cloud-connection)!: refuse an unparseable request body with 400 INVALID_REQUEST instead of reading it as {} #24005) do not change.What changes is which bodies the reference refuses: malformed declared JSON, and a non-empty body in an unread media type. It also starts parsing multipart fields. That makes the reference meet a contract it was behind, inside a package no deployment runs.
The dev re-judges the line from the diff with
scripts/pm/clause2-line.mjs.Responsibility:
packages/qa/http-conformance's reference adapter (this lane,packages/qain the lane table) | the referenceIHttpServerinvents{}for malformed JSON and for a multipart body with content, and hands atext/*body over as a string. The standard transport refuses or parses those, so the suite that exists to lock cross-adapter behaviour cannot see that difference | the conformance suite and every PR it gates; no product reach (triage6228186656)Thread-read: 6228186656
Serial constraints cleared:
The 6 open PRs read in this act (fix(cli): os serve says once when it cannot write its runtime state file, naming the file and the OS error #24234, fix(cli,runtime): a dangling symlink as the SQLite database's directory is refused at once by os dev and os serve, naming the file and the directory #24233, feat(spec): the remaining getSession readers read the declared AuthSessionApi result (user.isPlatformAdmin, role, name); the top-level activeOrganizationId fallback is removed #24232, fix(cloud-connection): report the release discovery reports as runtime_version when OS_RUNTIME_VERSION is unset #24231, feat(spec): ComponentPropsMap gains a 'list-view' row — a page-embedded list view is judged as a list view; stored objectName / viewType convert to data / type (#22444) #24217, chore: version packages (next) #24168): none touches
packages/qa/http-conformance/orplugin-hono-server'srequest-body.ts/adapter.ts.The open
pm:dispatchedcards of this lane read in this act are cloud-connection: the server reports the plugin's hardcoded 0.3.0 as its runtime_version to the cloud (bind/start verification URI and bind body) when OS_RUNTIME_VERSION is unset #24150 (this seat, in the queue) and seat 2's finding(mcp/oauth): an OAuth-token MCP agent acting for an ordinary user is denied every object operation — the D10 delegator is resolved with no active organization, so the user's organization-scoped grants do not count #24137, [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 (13b), finding(forms): the CRM example and the submit route's docstring say theguest_portalprofile authorizes an anonymous form submit; measured on 17.7.0, it does not #24130, cli/serve: when the runtime state file cannot be written, os serve still prints "Press Ctrl+C to stop" and says nothing; a supervisor then gets ENOENT on runtime.<env>.<project>.json (writeRuntimeState's catch is silent) #24091, cli/runtime: a dangling symlink in the SQLite database directory's path fails os dev and os serve late, with a plugin-start error that names neither the file nor the directory (the two bounded-walk copies accept any EEXIST) #24090 and runtime: the keys and actions domains answer 405 METHOD_NOT_ALLOWED with no Allow header (deps.error), where RFC 9110 requires one and the mcp, meta and ai domains send it #24023. None declares apackages/qa/http-conformance/path.objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{
"issue": 24083,
"status": "done",
"branch": "claude/issue-24083-conformance-adapter-body",
"pr": "#24242",
"session": "session_01BVm1pAXVXQ3PjC42j1kPFf — subagent run, so this is the parent's id (the relay resolved it from CLAUDE_CODE_REMOTE_SESSION_ID)",
"premise_still_valid": true,
"summary": "Premise held on origin/main 2bcd406 on all three counts, measured with a probe over a real socket. Malformed declared JSON answered 200 {}, multipart answered {}, and text/plain came through as a string. Two more divergences turned up: an empty text/plain body read as an empty string, and a urlencoded b[] key kept only its last value. packages/qa/http-conformance/src/adapter.ts now mirrors readRequestBody's decision inside the adapter, not imported (seat ruling MATCH). The four refusal messages are copied verbatim. Forms are parsed by Node's built-in Response.formData() in hono 4.13.12's field shape. req.body is a getter/setter that throws the refusal on read. rawBody() caches the read as one promise, so a refused body still yields its bytes. No dependency or export was added. Mechanism assumption 1 was partly falsified. The standard transport answers the refusal through a dedicated runHandler arm (refusedBodyRead) that overrides whatever the handler did, a caught throw included. It does not go through the generic declared-throw path. The reference rendered a thrown {status:400, code:INVALID_REQUEST} as 500 {error}, so the smallest answer-side change was added: on a matched route, writes after a refused read are dropped and writeBodyRefusal answers 400 INVALID_REQUEST in the same two-key envelope. The generic catch arm and the fallback branch's answer are untouched. Assumptions 2, 3 and 4 held. After the change, all 17 probe shapes answer the same status and body bytes on both adapters. New request-body.conformance.test.ts pins this on both adapters plus a 10-row byte-parity block. adapter.test.ts is unchanged: no assert pinned an old answer. No changeset (private package); skip-changeset is applied and read back. Clause-②: no. The card's assignee was not touched.",
"tests": "Final head 4bf994d (origin/main 669296b merged in). Closure built under os-verify-lock with pnpm turbo run build --filter='@objectstack/http-conformance^...' --concurrency=2: VERDICT command-exit 0, 36/36 tasks. pnpm --filter @objectstack/http-conformance exec vitest run --reporter=verbose: TEST_EXIT=0, Test Files 15 passed (15), Tests 174 passed (174). The suites that drive the reference and read body all pass: adapter.test.ts 16, fallback-seam.conformance.test.ts 18, conformance.integration.test.ts 30, query-multiplicity.conformance.test.ts 8, response-observation.conformance.test.ts 14, and the new request-body.conformance.test.ts 24. pnpm --filter @objectstack/http-conformance typecheck: TYPECHECK_EXIT=0. check:test-typecheck still holds 3 files / 27 errors / 10 signatures, unchanged. The new file is in the test program (15 of 15 test files under --listFiles) and has no ledger entry, so it has zero errors. test:repo, each exit captured separately, all exit 0: core 6 files / 86 tests; objectql 1/5; rest 5 files, 262 passed, 14 skipped; runtime 3/808; types 1/11; spec 67/1115 (VERDICT command-exit 0). Ablation, run once through node scripts/ablation-replace.mjs in WRAP mode plus an EXIT INT TERM trap. It restored the old arm (let body: any = {}; try { body = text ? JSON.parse(text) : {}; } catch { body = {}; }) in place of the refusal. On-disk proof: the anchor-marker count went 1 to 0, the old-arm count 0 to 1, and the blob 8204c4fa1a35 to 9b952dea3e0e. No build was needed: the suite imports ./adapter.js from source. RED: pnpm --filter @objectstack/http-conformance exec vitest run --maxWorkers=2 src/request-body.conformance.test.ts gave VERDICT command-exit 1, Tests 2 failed / 22 passed (24). The two failures were the node adapter's 'a malformed declared-JSON body is refused 400 INVALID_REQUEST at the read, never read as {}' and the parity row '/echo · malformedJson'. Every hono leg stayed green. Restore proven: the blob equals the HEAD blob (8204c4fa1a35) and git diff HEAD is empty. GREEN: same command on 9e5b8b1 gave VERDICT command-exit 0, Tests 24 passed (24).",
"gates": "Local, on 4bf994d: pnpm exec eslint --no-inline-config on adapter.ts and request-body.conformance.test.ts exited 0 with no output. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack exited 0 and derived 52 commands; each exit was captured before any pipe. 51 exited 0. pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: it reads every package's dist/ and only this closure is built locally), so it is NOT MEASURED locally and runs in CI's Lint & Repo Gates. dispatch-gates --ran exited 0: '52 derived famil(ies) accounted for — 51 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. node scripts/check-changeset-no-major.mjs --base 2bcd406 --event (PR body) exited 0: no major; LEVEL AXIS declared no, no direction arm. scripts/pm/clause2-line.mjs has no CLI: run bare it printed nothing and exited 0. Its readClause2Line export reads the body line as {kind:declared, value:no, arm:null}. No narrowing face was read. A self-scan for control bytes on both files found no match. check-platform-checklist --changed-from was not run: no exported symbol was renamed or deleted (isEagerlyParsed was module-private). PR #24242 check-runs on 4bf994d at report time: success for Check Changeset, Governed Surface Queue Guard, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Type Check · workspace, Type Check · debt ledger, Dogfood Regression Gate, Check Documentation Links, Flag docs affected by code changes, and the claim/closing-keyword guards. in_progress for Build Core, Test Core (1/6 to 6/6), Lint · ESLint, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · docs & code guards, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Type Check · source gates, and Type Check · consumer gates. CI convergence is left to PM; not waited on.",
"mcp_calls": "0",
"api_writes": "3 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]. (1) pr_create, run 39086404034: POST /repos/objectstack-ai/objectstack/pulls (draft) plus POST /repos//issues/24242/assignees [os-sales]. The body read back byte-identical. (2) label-write labels_add, run 39086451585: POST /repos//issues/24242/labels [skip-changeset]. Read back as size/m and skip-changeset. (3) This os-dev-report comment: POST /repos//issues/24083/comments. git push x4 to claude/issue-24083-conformance-adapter-body (the empty-branch probe, the fix, the test, the merge of origin/main); these are not REST writes.",
"open_questions": [
{
"question": "packages/qa/http-conformance/README.md line 44 lists 'multipart parsing (binary bodies stay raw behind the lazy req.rawBody())' as deliberately absent. This diff makes that false. The README is outside the claim's file surface, so it was not edited. Who corrects it?",
"options": [
"A: the seat authorises a one-line README correction riding PR #24242 (a dev patch round or the seat's own commit)",
"B: a follow-up docs-only change after #24242 lands"
],
"recommendation": "A, because this PR is what makes the line false. A one-line fix in the same landing avoids a window where the package's own README contradicts its code. It is private-package prose with no gate and no consumer."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted in PR Acceptance notes, not filed. The fallback seam (held off) is now a cross-adapter divergence. Its req is built by the same code as a route's (guarantee 2), so a fallback that reads a refused body meets the throw. The reference answers its own 500 'Fallback handler failed'; the standard transport (whose fallback also runs through runHandler) answers 400 INVALID_REQUEST. Private instrument, no public door, no named product producer, so no reach and no class. Dedupe words: fallback seam refused body 500 Fallback handler failed vs 400 INVALID_REQUEST; NodeHttpServer setFallbackHandler body refusal parity",
"carrier: 承接者:无 · noted, not filed. A handler that writes before reading a refused body cannot have its answer replaced on node:http (the bytes are already sent); the standard transport buffers and replaces. Documented in writeBodyRefusal's docblock; no conformance case reaches it.",
"carrier: 承接者:无 · noted, not filed. The reference hands use() middleware the real req, so a middleware reading req.body meets the refusal; the standard transport hands middleware an empty request. The middleware contract does not populate body.",
"carrier: 承接者:无 · noted, not filed. Pre-existing: every reference JSON answer carries Content-Type 'application/json; charset=utf-8', while the standard transport's carry 'application/json'. Body bytes and status match."
],
"deviations": [
"spec test:repo ran past the 600s foreground tool cap and the harness moved that call to the background. I waited on its completion and read its VERDICT line (command-exit 0, 67 files / 1115 tests). I started no watcher.",
"skip-changeset was written with label-write.mjs as a separate stroke, as the dispatch budget and my loaded os-dev instructions say. The .claude/agents/os-dev.md on main says skip-changeset should ride pr_create's labels instead. The two texts conflict; flagged, not resolved here.",
"The fallback seam's body changed by construction (one request builder, its guarantee 2); its answer path was not touched. Explained in the PR's Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Seat ruling on
os-dev-reportround 0 (PR #24242, head4bf994d77) and amendment to claim6243784478·domain:cliseat 1 ·os-sales· sessionsession_01BVm1pAXVXQ3PjC42j1kPFf· 2026-10-18T01:31Z · ⛔ not a new claim; the session, branch and dev stayThe seat read the diff on
4bf994d77before ruling. The route branch mirrorsreadRequestBodyas ruled (MATCH, not imported), and the answer-side change insideadapter.tsis the smallest that the falsified assumption 1 calls for. Two items move into a patch round:The report's open question (the README line): A.
packages/qa/http-conformance/README.md's line that lists multipart parsing as deliberately absent becomes false with this PR, so the one-line correction rides it. The dev makes it in the patch round.The fallback seam's answer to a refused read: the claim's hold-off is lifted for the ANSWER path only. The report's first out-of-scope note found a new cross-adapter split:
500 Fallback handler failedwhen the fallback reads a refused body.runHandleras its routes, so itsrefusedBodyReadarm answers400 INVALID_REQUESTthere too.dispatcher-plugin.tsinstalls it (:2033,:2048), and every metadata-declaredapis:endpoint runs through it.So the suite this card exists to fix would stay blind on the door the dispatcher bridge actually uses. Four axes:
Hence A over carrying it as a note.
Amended file surface (all else in the claim stands):
adapter.ts: the fallback branch's answer to a read of a refused body, mirroring the standard transport'srunHandler. Measure its arms first, on both adapters, through a fallback that reads a refused body and then:Guarantee 2 ("
req.bodyIS readable", one request builder) and guarantee 4 (a fallback that writes nothing and read no refused body leaves the 404 / 405) stay as they are.request-body.conformance.test.ts: fallback legs for those arms on both adapters, plus one positive control.fallback-seam.conformance.test.tsis not edited unless one of its asserts pins the old answer (say which).packages/qa/http-conformance/README.md: the one line.⛔ Still held off:
packages/spec/**;packages/plugins/plugin-hono-server/**; the middleware (use()) request; hono's multipartrawBody()cache.The other out-of-scope notes stand as
carrier: none:node:http, documented inwriteBodyRefusal;body);; charset=utf-8difference, where status and bytes match.The dev's deviation on how
skip-changesetis written (a separatelabel-writestroke, whereos-dev.mdonmainsayspr_create's labels) is noted for the seat's round report. The label is on the PR either way.