Skip to content

fix(plugin-hono-server): surface repeated query parameters as arrays (#6878 route 2) - #7396

Merged
os-help merged 1 commit into
mainfrom
claude/issue-6878-hono-queries-route-2
Aug 10, 2026
Merged

fix(plugin-hono-server): surface repeated query parameters as arrays (#6878 route 2)#7396
os-help merged 1 commit into
mainfrom
claude/issue-6878-hono-queries-route-2

Conversation

@os-help

@os-help os-help commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

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):

Seat ruling on the open fork (cli lane, session session_0158ZQo7LiHSxGWpYKuPq1wu): route 2 is adopted — but its ORDER relative to #6877 is reversed from what the previous seat wrote. #6877's sweep goes FIRST.

And the ordering constraint, which is why this PR lands now and not in August's first week:

Today the Hono adapter collapses duplicates before the handler sees them, so #6877's 52 unguarded read points cannot receive an array in production — the defect surface exists but is dormant on the production server. Route 2's whole point is to stop collapsing. The moment it lands, those 52 points start receiving arrays

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 — both IHttpRequest.query construction sites move to one readQuery(c) helper:

  • the route-handler seam (was line 299)
  • the use() middleware seam (was line 743)

The helper reads c.req.queries() and normalises by length: values.length > 1 ? values : values[0]. Built with Object.fromEntries rather 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.

⚠️ The normalisation is not optional, which is the tripwire #6941 left: c.req.queries() returns an array for every key, single-valued ones included.

Independently confirming the seat's "two sites, no third" measurement: grep for req.query() / req.queries() across packages/ and examples/ now returns zero production call sites of c.req.query() repo-wide. Every other implements IHttpServer is a capture-only mock that never parses a query string.

packages/spec is untouched — the declared Record< 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. No domain:spec finding 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.ts was written under route 1 as a divergence record with a per-adapter measuredQuery field, and its header carried explicit collapse instructions for the day the fork was decided:

collapse the per-adapter measuredQuery rows into one shared expectation and delete the divergence describe.

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:

  • The single-value control case stays, marked load-bearing. It is the only assertion separating "arrays on repeats" from "arrays on everything".
  • A new middleware-seam case, per adapter. Route 2 is a two-point change on Hono, and without this every existing case would stay green if only the route-handler seam were flipped.

Reverse verification

Directions predicted before running, then measured.

Variant Predicted Measured
BEFORE — unmodified adapter, unmodified test on origin/main passes; the divergence recording still holds 6 passed
AFTER — adapter flipped, test collapsed 8 passed (3 per-adapter cases x 2 adapters + 2 cross-adapter) 8 passed
(a) adapter reverted to c.req.query(), tightened test kept 4 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 key 4 failed / 4 passed, those exact four ✅
(b) bare c.req.queries(), no normalisation 4 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 inspects version, an array either way 4 failed / 4 passed, those exact four ✅

Variant (b)'s control-case failure reproduced #6941's predicted message verbatim:

AssertionError: expected { single: [ '9' ] } to deeply equal { single: '9' }

Note the counts differ from #6941's 4 failed | 2 passed for 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 (never git stash), and after restoring, git diff was byte-compared against the saved patch — identical.

Local verification

pnpm --filter '@objectstack/plugin-hono-server' test    →  16 files, 187 passed
pnpm --filter '@objectstack/http-conformance' test      →   4 files,  72 passed
pnpm --filter '@objectstack/hono' test                  →   2 files,  73 passed
pnpm --filter (both packages) typecheck                 →  tsc --noEmit, Done
npx eslint (both changed files)                         →  clean
node scripts/check-nul-bytes.mjs                        →  OK (6775 files)

@objectstack/hono is 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, and plugin-hono-server itself rebuilt before each conformance run — http-conformance resolves it through dist/, so a stale build would have measured the wrong adapter in every variant above.

pnpm check:type-check-debt could not complete locally: its --re-measure half 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 printed OK — 63/77 workspace packages type-checked, and neither changed package is in the DEBT ledger. CI builds the closure before that step.

Changeset

patch for both packages. The plugin-hono-server entry 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

…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
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 9:29am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-hono-server.

5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/getting-started/your-first-project.mdx (via @objectstack/plugin-hono-server)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-hono-server)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-hono-server)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-hono-server)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/plugin-hono-server)

2 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-hono-server)
  • content/docs/releases/v16.mdx (via @objectstack/plugin-hono-server)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 10, 2026
@os-help
os-help marked this pull request as ready for review August 10, 2026 10:01
@os-help
os-help added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 7cdbcbb Aug 10, 2026
26 checks passed
@os-help
os-help deleted the claude/issue-6878-hono-queries-route-2 branch August 10, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

两个 IHttpServer 适配器对「重复的查询参数」给出不同形状:Hono 折叠成第一个值,node:http 给数组

2 participants