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
Activity
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 commented
on Aug 12, 2026 CollaboratorMore actionsMaintainer ruling — 2026-08-12
裁定:政策 YES —— 闭合查询参数集成为 REST ingress 政策;采纳方式为增量,⛔ 不打大包。
要点:
- 新路由自落地之日起必须声明闭合参数集(用 [approvals]
assignedToMe=trueis not a supported list filter on/api/v1/approvals/requests— silently ignored, returns every request #7527 引入的refuseUnknownQueryParams),这是 review 可执行的规则。 - 存量路由按 lane 顺手转换,不做 one-shot wave;data 读路由优先(静默变宽/变窄在那里咬得最狠 ——
?filter被丢返回全量、where内键被丢返回 200/0,两个方向 AI 调用方都无从察觉)。 - 每个路由的闭集必须从 handler 实读点测出,不许猜 —— 漏掉
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 收到的实参)成对。 - 破坏被容忍流量是有意为之:v17 是最便宜的破窗窗口,越晚存量越大。
- 与 Data query: an unknown field inside
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
- 新路由自落地之日起必须声明闭合参数集(用 [approvals]
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.⚠️ Serial constraint the ruling could not have known aboutPR #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:- Branch from a
mainthat contains it. ⛔ Do not undo or reroute that refusal. - The two guards must compose deliberately. A request that is both repeated and unknown must produce one defined answer — decide which fires first, pin it, and say why in the PR. Two refusals racing on one request is how a third dialect appears.
⚠️ They currently answer different codes: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
- Policy in writing, review-enforceable: a new route declares its closed parameter set from landing day.
- Data read routes first. Silent widening/narrowing bites hardest there — a dropped
?filterreturns the full set, a dropped key insidewherereturns200/0 rows, and an AI caller can detect neither direction. - ⛔ Measure each route's closed set from the handler's actual read points. Never guess. Missing
limitconverts a silent-widening bug into a loud pagination incident — a worse outcome than the defect. - [approvals]
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. - Envelope and message shape must match Data query: an unknown field inside
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. - ⛔ No one-shot wave. Land the policy plus a first tier you can pin properly. A broad sweep with thin pins is the failure mode this ruling explicitly rejected.
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
- Branch from a
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/requestsnow declares a closed query-parameter set and refuses anything outside it with a located400. The card that produced it explicitly suggested going wider — "worth fixing as one policy (reject unknown query parameters with a located400) 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.tshandlers 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-looking200. The failure is undetectable from the response in both directions, which is what makes it worth a policy rather than a bug-per-endpoint: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).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:
refuseUnknownQueryParamsinpackages/rest/src/query-allowlist.ts, alongside the multiplicity rulerefuseRepeatedQueryParams(#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:
limitconverts a silent-widening bug into a loud paging outage.rest-server.tshas roughly 50 query read points; each needs reading, not a sweep.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.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.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
statusplus the nestederror.codeplus that the service was never called, and preservation cases assert the argument the service was handed, not merely a200).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
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 introducedwhere/$filteranswers 200/0 instead of400 INVALID_FIELD— the bare-key door disagrees (#4134's uncovered sibling) #7534 — the same anti-pattern on thewhere/$filterfacepackages/rest的其它req.query.*读取点同样把string | string[]当字符串用(#6307 的未扩大部分) #6877 — the multiplicity rule this reuses the shape and envelope ofGenerated by Claude Code