fix(plugin-hono-server): surface repeated query parameters as arrays (#6878 route 2) - #7396
Conversation
…6878) Flip both of the Hono adapter's IHttpRequest.query construction sites — the route-handler seam and the use() middleware seam — from c.req.query() (first value per key) to c.req.queries() through one readQuery(c) helper that normalises by length: a repeated key becomes an array, a single-valued key stays a plain string. This is #6878 route 2, adopted by the cli-lane seat on 2026-08-10. It removes the divergence PR #6941 recorded: the reference NodeHttpServer already kept arrays, so one request had two answers depending on which server booted, and a handler could not refuse an ambiguity the transport had already collapsed. The normalisation is load-bearing: c.req.queries() returns an array for EVERY key, single-valued ones included, so a bare swap would have turned every existing single-value read point on the production adapter into an array. Also collapses #6941's divergence-recording conformance case into the single expected shape, per that file's own header instructions, and adds a middleware-seam case so a half-applied two-site change cannot pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0158ZQo7LiHSxGWpYKuPq1wu
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
Fixes #6878
Implements route 2 of #6878: on the Hono server, a repeated query parameter now surfaces as an array; a single-valued key stays a plain string. Both halves the card needs are here — the adapter flip and the conformance-case tightening — so this closes it. Route 1 landed earlier as
Part of(PR #6941); route 3 was ruled out.The ruling this implements
Quoted verbatim from the cli-lane seat ruling, #6878 comment 5236010909 (2026-08-10):
And the ordering constraint, which is why this PR lands now and not in August's first week:
The prerequisites are landed. #6877 shipped via PR #7324 (24 gate sites, 63 single-valued parameter slots that refuse repetition, 8 parameters classified genuinely multi-valued and pinned); #7321 shipped via PR #7386. So the rest-side surface is already gated — this PR is what ACTIVATES those gates on the production adapter. Until now they guarded a shape the Hono transport never produced.
The change
packages/plugins/plugin-hono-server/src/adapter.ts— bothIHttpRequest.queryconstruction sites move to onereadQuery(c)helper:use()middleware seam (was line 743)The helper reads
c.req.queries()and normalises by length:values.length > 1 ? values : values[0]. Built withObject.fromEntriesrather than dynamic property writes, mirroring the reference adapter's own query block — the keys come off the wire, and?__proto__=…through a dynamic write is remote property injection.c.req.queries()returns an array for every key, single-valued ones included.Independently confirming the seat's "two sites, no third" measurement:
grepforreq.query()/req.queries()acrosspackages/andexamples/now returns zero production call sites ofc.req.query()repo-wide. Every otherimplements IHttpServeris a capture-only mock that never parses a query string.packages/specis untouched — the declaredRecord< string, string | string[] >union already permitted arrays, so nothing about the contract's type moved. What moved is the platform's answer, from "depends on which server booted" to one answer. Nodomain:specfinding was filed: the convention is now enforced by an executable cross-adapter gate, which is a stronger record than prose in the contract would be.Conformance tightening
packages/qa/http-conformance/src/query-multiplicity.conformance.test.tswas written under route 1 as a divergence record with a per-adaptermeasuredQueryfield, and its header carried explicit collapse instructions for the day the fork was decided:Done exactly that: one shared
EXPECTED_QUERY, and the divergence describe replaced by an agreement describe. The header was rewritten to say what the file is now (a contract), what it was (a record), and that a future red is a regression rather than a reminder.Two things deliberately kept or added:
Reverse verification
Directions predicted before running, then measured.
origin/main6 passed✅8 passed(3 per-adapter cases x 2 adapters + 2 cross-adapter)8 passed✅c.req.query(), tightened test kept4 failed / 4 passed. Red: hono repeated-key, hono middleware-seam, both cross-adapter cases. Hono control case stays GREEN —query()already returns a string for a single key4 failed / 4 passed, those exact four ✅c.req.queries(), no normalisation4 failed / 4 passed. Red: hono repeated-key, the hono control case, hono middleware-seam, cross-adapter "agrees on BOTH". The cross-adapter "SAME operand" case passes here — it only inspectsversion, an array either way4 failed / 4 passed, those exact four ✅Variant (b)'s control-case failure reproduced #6941's predicted message verbatim:
Note the counts differ from #6941's
4 failed | 2 passedfor the same mistake only because this revision has 8 cases where that one had 6.The asymmetry between (a) and (b) is the point worth keeping: neither variant is caught by the same set of cases. Reverting the flip leaves the control case green; forgetting the normalisation leaves the cross-adapter operand case green. Both are needed.
The fix was taken out with
git checkout/git apply(nevergit stash), and after restoring,git diffwas byte-compared against the saved patch — identical.Local verification
@objectstack/honois included because it is the one downstream package that wraps this adapter rather than mocking it. Dependencies were built with the^...(upstream) filter before typechecking, andplugin-hono-serveritself rebuilt before each conformance run —http-conformanceresolves it throughdist/, so a stale build would have measured the wrong adapter in every variant above.pnpm check:type-check-debtcould not complete locally: its--re-measurehalf refuses to run without the full workspace build closure on disk (4 unrelated packages have no built type entry point in a fresh worktree — the guard from #6376). Its coverage half printedOK — 63/77 workspace packages type-checked, and neither changed package is in the DEBT ledger. CI builds the closure before that step.Changeset
patchfor both packages. Theplugin-hono-serverentry states the behaviour change plainly rather than as a refactor — it is the ruled intent — and carries the migration note for anyone reading query parameters off the Hono server.Generated by Claude Code