Skip to content

[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

@os-help

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:

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:

  1. 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 limit converts a silent-widening bug into a loud paging outage. rest-server.ts has roughly 50 query read points; each needs reading, not a sweep.
  2. 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.
  3. 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 / datasource pool 声明在 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.
  4. Scope boundary with Data query: an unknown field inside where / $filter answers 200/0 instead of 400 INVALID_FIELD — the bare-key door disagrees (#4134's uncovered sibling) #7534. That issue covers unknown fields inside where / $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


Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Triage: needs-user-decision + domain:cli, not target: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:cli per the domain table.

    Premise check on origin/main @ 2c1988c. One correction to the card's mechanism claim: packages/rest/src/query-allowlist.ts does 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 helper refuseUnknownQueryParams arrives 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

  2. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    Collaborator

    Maintainer ruling — 2026-08-12

    裁定:政策 YES —— 闭合查询参数集成为 REST ingress 政策;采纳方式为增量,⛔ 不打大包。

    要点:

    裁定人:维护者 huangyiirene(2026-08-12,接受 PM 综合分析后批准);由 PM 会话 session_01GZKbx4xyF7U5WXj6ch49BM 代笔落卡。转 pm:queue(首批范围 = 政策落文 + data 读路由第一梯队)。


    Generated by Claude Code

  3. self-assigned this
    on Aug 12, 2026
  4. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Claimed by the domain:cli PM seat (#6024, session session_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:102 exports refuseUnknownQueryParams, with rest-server-approvals-unknown-filter.test.ts alongside 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 about

    PR #8004 (#7390) merged into the GET /data/:object handler earlier today: a repeated ?filter= is now refused with 400 INVALID_FILTER naming repetition. That is the same handler this card's first tier targets, so:

    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

    1. Policy in writing, review-enforceable: a new route declares its closed parameter set from landing day.
    2. Data read routes first. Silent widening/narrowing bites hardest there — a dropped ?filter returns the full set, a dropped key inside where returns 200/0 rows, and an AI caller can detect neither direction.
    3. ⛔ Measure each route's closed set from the handler's actual read points. Never guess. Missing limit converts a silent-widening bug into a loud pagination incident — a worse outcome than the defect.
    4. [approvals] assignedToMe=true is 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.
    5. Envelope and message shape must match Data query: an unknown field inside where / $filter answers 200/0 instead of 400 INVALID_FIELD — the bare-key door disagrees (#4134's uncovered sibling) #7534's where / $filter face — check it, do not assume.
    6. ⛔ 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions