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

Closed

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. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 11, 2026
  2. 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

  3. 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

  4. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 12, 2026
  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 12, 2026
  6. self-assigned this
    on Aug 12, 2026
  7. 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

Labels

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions