Repository navigation
[rest] Unknown query parameters are silently dropped on every REST route except /approvals/requests — decide whether the closed-parameter-set rule becomes ingress policy #7606
Description
What this is
A scope spill-out from #7527, filed for triage rather than fixed there (Prime Directive #10).
#7527 fixed exactly one route: GET /api/v1/approvals/requests now declares a closed query-parameter set and refuses anything outside it with a located 400. The card that produced it explicitly suggested going wider — "worth fixing as one policy (reject unknown query parameters with a located 400) rather than one endpoint at a time" — and that wider change was deliberately left out of #7527's PR because it is a cross-lane REST-ingress policy decision, not an approvals bug.
The condition, stated generally
rest-server.ts handlers read the query keys they know and ignore the remainder. Every route other than the one #7527 closed still does this, so any misspelled, renamed or invented parameter is silently dropped and the caller gets a plausible-looking 200. The failure is undetectable from the response in both directions, which is what makes it worth a policy rather than a bug-per-endpoint:
- silently widens — a dropped filter returns more rows than asked for, and an unfiltered list is shaped exactly like a genuinely broad match ([approvals]
assignedToMe=trueis not a supported list filter on/api/v1/approvals/requests— silently ignored, returns every request #7527's measured case:?assignedToMe=truereturned every request). - silently narrows — a dropped key inside
where/$filteranswers 200/0 (QA run · api-backend (FULL area) · a86db175 · 2026-08-10 · 6 PASS / 2 PARTIAL / 3 FAIL #7463 defect 2, tracked as Data query: an unknown field insidewhere/$filteranswers 200/0 instead of400 INVALID_FIELD— the bare-key door disagrees (#4134's uncovered sibling) #7534).
The mechanism to do it with already exists and is proven on one route: refuseUnknownQueryParams in packages/rest/src/query-allowlist.ts, alongside the multiplicity rule refuseRepeatedQueryParams (#6877) that established the same shape and the same ADR-0112 envelope.
Why it is a decision and not a chore
Applying the rule to the remaining routes is mechanical per route, but the policy has real costs to weigh, and getting them wrong is worse than the current silence:
- Every route's closed set must be MEASURED, not guessed. The set is not "the filters" — it includes paging, ordering, alias spellings, and anything middleware reads. A whitelist that forgets
limitconverts a silent-widening bug into a loud paging outage.rest-server.tshas roughly 50 query read points; each needs reading, not a sweep. - It is a breaking change for tolerated traffic. Any client today sending a parameter we ignore starts getting a
400. That traffic is invisible to us precisely because we drop it silently, so the blast radius cannot be measured from our side — only decided. - Third-party and forward-compat callers. A caller written against a newer client sending a parameter an older server does not know currently degrades quietly; under the policy it fails hard. That may well be the right answer (it is the datasource
pool声明在 sqlite / sqlite-wasm 驱动臂被静默丢弃(pg / mysql 生效) #5714 / datasourcepool声明在 memory 驱动臂同样被静默丢弃(#5714 的姊妹臂,裁决未覆盖) #5931 / QA run · api-backend (FULL area) · a86db175 · 2026-08-10 · 6 PASS / 2 PARTIAL / 3 FAIL #7463 family norm), but it is a deliberate stance on API evolution, not an implementation detail. - Scope boundary with Data query: an unknown field inside
where/$filteranswers 200/0 instead of400 INVALID_FIELD— the bare-key door disagrees (#4134's uncovered sibling) #7534. That issue covers unknown fields insidewhere/$filter— the body/filter face. This one is the query-string face. They share a principle and should probably agree on envelope and message shape, but they are different code paths.
Suggested shape, if it is taken
Route-by-route adoption of the existing helper, sized per lane rather than as one PR — each route's closed set measured from its own handler reads, with preservation pins alongside the refusal pins (the #7527 test file is the template: refusal cases assert status plus the nested error.code plus that the service was never called, and preservation cases assert the argument the service was handed, not merely a 200).
No behaviour is claimed broken beyond the routes themselves; nothing here is urgent. Filing so the policy question is on the board instead of living only inside a closed approvals card.
Refs
- [approvals]
assignedToMe=trueis not a supported list filter on/api/v1/approvals/requests— silently ignored, returns every request #7527 — the one route already closed, and the helper it introduced - Data query: an unknown field inside
where/$filteranswers 200/0 instead of400 INVALID_FIELD— the bare-key door disagrees (#4134's uncovered sibling) #7534 — the same anti-pattern on thewhere/$filterface packages/rest的其它req.query.*读取点同样把string | string[]当字符串用(#6307 的未扩大部分) #6877 — the multiplicity rule this reuses the shape and envelope of
Generated by Claude Code
Activity
os-zhuang commentedon Aug 11, 2026
Triage:
needs-user-decision+domain:cli, nottarget:v17.Classification. This is a genuine policy fork, not a chore: applying the closed-parameter-set rule repo-wide is a breaking change for tolerated traffic whose blast radius the card correctly says "cannot be measured from our side — only decided", plus a deliberate stance on forward-compat callers. That is a maintainer call (API-evolution semantics), so it goes to the decision box rather than the queue. The card's own four-point cost analysis is the decision material.
Landing site (read, not guessed). The per-route work would land in
packages/rest/src/rest-server.ts(~50 query read points per the card) using the allowlist-helper shape ⇒domain:cliper the domain table.Premise check on
origin/main@2c1988c. One correction to the card's mechanism claim:packages/rest/src/query-allowlist.tsdoes not exist on main yet (counter-probe:packages/rest/src/query-multiplicity.ts— the #6877 rule the card cites — does exist, so the scan itself is sound). The helperrefuseUnknownQueryParamsarrives with #7527's PR, which is still in flight (pm:dispatched). The decision is takeable now; any execution card cut from a ruling would be blocked-in-fact on that PR landing.Dedup. #7534 is the
where/$filter(body/filter) face and this is the query-string face — same principle, different code paths, correctly cross-referenced rather than duplicated. #6877 (multiplicity) is the closed precedent this reuses. No open card covers the query-string policy question; nothing to converge.Release board. Not
target:v17: no defect is claimed beyond the already-tracked per-route cards; this is a policy/enhancement question (default non-blocking).本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
Maintainer ruling — 2026-08-12
裁定:政策 YES —— 闭合查询参数集成为 REST ingress 政策;采纳方式为增量,⛔ 不打大包。
要点:
assignedToMe=trueis not a supported list filter on/api/v1/approvals/requests— silently ignored, returns every request #7527 引入的refuseUnknownQueryParams),这是 review 可执行的规则。?filter被丢返回全量、where内键被丢返回 200/0,两个方向 AI 调用方都无从察觉)。limit会把静默变宽 bug 换成响亮的分页事故。[approvals]assignedToMe=trueis not a supported list filter on/api/v1/approvals/requests— silently ignored, returns every request #7527 的测试文件是模板:refusal pin(status +error.code+ service 未被调用)与 preservation pin(service 收到的实参)成对。where/$filteranswers 200/0 instead of400 INVALID_FIELD— the bare-key door disagrees (#4134's uncovered sibling) #7534(where/$filter面)共原则:两边的 envelope 与 message 形状应一致,实现时互相对照。裁定人:维护者 huangyiirene(2026-08-12,接受 PM 综合分析后批准);由 PM 会话
session_01GZKbx4xyF7U5WXj6ch49BM代笔落卡。转pm:queue(首批范围 = 政策落文 + data 读路由第一梯队)。Generated by Claude Code
hotlong commentedon Aug 12, 2026
Claimed by the
domain:cliPM seat (#6024, sessionsession_01B3Kurx8qufrDzNjk4rag7V).Branch:
claude/issue-7606-closed-query-param-ingress-policy· dispatch:mode:cloud,model: claude-opus-5.The blocker is cleared — the triage note is now stale
The triage comment records that
packages/rest/src/query-allowlist.ts"does not exist on main yet" and that execution would be blocked-in-fact on #7527's PR. #7527 has landed:query-allowlist.ts:102exportsrefuseUnknownQueryParams, withrest-server-approvals-unknown-filter.test.tsalongside it. Re-verified before dispatch. Scope is the maintainer's first batch: policy documentation + the first tier of data read routes.PR #8004 (#7390) merged into the
GET /data/:objecthandler earlier today: a repeated?filter=is now refused with400 INVALID_FILTERnaming repetition. That is the same handler this card's first tier targets, so:mainthat contains it. ⛔ Do not undo or reroute that refusal.INVALID_FILTER(by the maintainer's finding: a repeated?filter=onGET /data/:objectcannot be told from a filter AST, so it is diagnosed as a malformed filter (and, rarely, succeeds) #7390 ruling) vs whateverrefuseUnknownQueryParamsanswers. A repeated?filter=answers two different error codes depending on which data route received it #8001 already records a fork of exactly this kind and is unresolved — ⛔ do not resolve it unilaterally here. If your composition forces the question, report it.Also in flight on this file: #7981, converging
registerSecurityEndpoints' envelope shapes. Different region, same hot file — rebase rather than resolve blind.Binding, from the ruling
?filterreturns the full set, a dropped key insidewherereturns200/0 rows, and an AI caller can detect neither direction.limitconverts a silent-widening bug into a loud pagination incident — a worse outcome than the defect.assignedToMe=trueis not a supported list filter on/api/v1/approvals/requests— silently ignored, returns every request #7527's test file is the template: a refusal pin (status +error.code+ the service was not called) paired with a preservation pin (the arguments the service actually received). Both halves, per route.where/$filteranswers 200/0 instead of400 INVALID_FIELD— the bare-key door disagrees (#4134's uncovered sibling) #7534'swhere/$filterface — check it, do not assume.Breaking tolerated traffic is deliberate and v17 is the intended window — say so in the changeset rather than describing this as a pure fix.
Generated by Claude Code
fix(service-datasource): forward `?schema=` on the admin remote-table…
feat(rest): closed query-parameter sets become ingress policy, first …