Skip to content

Approvals: GET /api/v1/approvals/requests?status=VALUE answers 200 with an empty list for a status that does not exist, instead of refusing it #29880

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbWT669ynWFKZjqFyNF

立卡门: ① 产品缺陷,具名落点 + 复现(reach: 公开入口 HTTP GET /api/v1/approvals/requests?status=bogus 实测一次错误结果,复测一致)
业务理由: 对接审批收件箱的集成(机器人、提醒脚本、第三方面板)把状态值拼错(Pending、waiting)时,拿到的是「没有待审」而不是错误,于是该提醒的审批被静默跳过。同一扇门对未知的参数名已经给出定位 400,对未知的取值却静默放行,是 #3948 家族的静默窄化。
动手的读者: domain:server 席,packages/rest/src/approvals-routes.ts 列表路由(~L262 只校验参数名,~L303 原样转发 q.status),按 sys_approval_request.status 的取值集校验并 400 指名允许值,附 HTTP 层 pin。
查重: repo is:issue approvals requests status 271 命中、approval list unknown status value 369(均含已关,前几页为 qa-run 记录与无关卡);最近邻 #27451(未知 request id 的 /actions 答 200 [],已关)不是本现象。

QA-source: #29877 · approvals.account-app-entry · observed beside the clauses

What happens

The approvals request list refuses an unknown query parameter NAME with a located 400, but accepts any status VALUE: GET /api/v1/approvals/requests?status=bogus answers 200 {"data":[]}. A caller cannot tell "nothing in that state" from "that state does not exist".

Measured at framework 589049b0 (main, 2026-11-03), stock showcase, as a non-admin approver and as admin, twice.

Reproduction

  1. Boot the stock showcase; sign in as any member who has pending approvals (or as admin).
  2. GET /api/v1/approvals/requests?status=pending → 200 with the pending rows (control).
  3. GET /api/v1/approvals/requests?status=bogus (or Pending).
  4. Expected: 400 naming status and its allowed values, as the door already does for an unknown parameter name.
  5. Actual: 200 {"data":[]}.

Mechanism (read at 589049b)

packages/rest/src/approvals-routes.ts: the list handler checks parameter names against an allow-list (~L262) and passes status: q.status (~L303) to the service unvalidated; the service filters on it and finds nothing.

Activity

  1. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p3 · domain:server · pm:queue · 2026-11-03T09:56Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 3, 2026
  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

    Claim: PM loop round 1
    Session: session_01Utp6CZMkHvuDeVYgTD73qF (seat) · dev card thread session_01Utp6CbAccy76jpUtQ9ZZX9
    Account: os-bill
    Branch: claude/project-thread-kjhqfe (the card thread's designated push branch, the recorded platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-29880
    Domain: domain:server
    Seat: domain:server#4
    File surface: packages/rest/src/approvals-routes.ts (the GET /api/v1/approvals/requests list route), its tests, one changeset. ⛔ packages/spec/**, docs/adr/**, .claude/**, repo-root AGENTS.md, ui/**, skills/** (stop on breach; explain in the report)
    Container & model: S, mode:thread, model: default judgment tier (opus) — dispatch-gates --tier: no path-derived mandate, effort: default
    Clause-②: yes (narrowing) — an unknown status value answered 200 [] and will answer 400 naming the allowed values; known values unchanged
    Metadata-protocol: none
    Responsibility: the approvals list route checks query parameter names but forwards q.status unchecked | none: GET /api/v1/approvals/requests?status=bogus answers 200 {"data":[]} (QA run #29877) | any integration polling the approvals inbox with a misspelled status, which silently skips pending approvals
    Thread-read: 6600556144
    Premise: holds (card measured at 589049b0; the dev re-measures first)
    Serial constraints cleared: seats 2, 3 and 5 confirmed no overlap; no card in flight edits approvals-routes.ts.

    Dispatch order: /mnt/project-files/server-seat4/dispatch-29880.md (project shared folder). Ruling section: the card and triage first-touch 6600556144 (p3, domain:server), dispatched on the maintainer's 「并发都到5」. BREAKING (!): a contract review and the maintainer's word come before landing. Fixes #29880.

    Posted 2026-11-05T12:17Z.

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
    os-dev-report

    Dev card thread session_01Utp6CbAccy76jpUtQ9ZZX9, branch claude/project-thread-kjhqfe, Claim 6657794401. Draft PR #30491 (author objectstack-fleet[bot]), head 976da77f.

    {
      "issue": 29880,
      "status": "done",
      "branch": "claude/project-thread-kjhqfe",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30491",
      "session": "session_01Utp6CbAccy76jpUtQ9ZZX9",
      "premise_still_valid": true,
      "summary": "Re-measured at c5800818 on the route's own handler with a recording service: ?status=bogus and ?status=Pending answered 200 {data:[]} with the string forwarded as is; ?status=pending,returned (what @objectstack/client approvals.listRequests sends for an array) was forwarded as ONE string and also answered 200 []; a repeated status was already refused 400 by the arity gate. Fix: module-private readApprovalStatusFilter in approvals-routes.ts splits on commas, trims, requires each entry to be one of APPROVAL_STATUSES (@objectstack/spec/contracts, from which sys_approval_request.status's options are built, #3786), and otherwise answers 400 with the nested VALIDATION_ERROR body the name and arity gates on the same handler use, naming each bad value once and every allowed value; the service is not called. Empty value / empty entry refused (today ?status= answers every status). One status reaches the service as a string, several as a deduped ApprovalStatus[] (service pushes $in); countRequests gets the same filter. Runs after the name and arity gates. Known status with no rows still 200 []. Seat agreed to the comma split, the contract source and the empty refusal before the gates ran.",
      "tests": "New packages/rest/src/rest-server-approvals-status-value.test.ts, 16 cases on the #7527 harness (real RestServer, registered handler, recording approvals service). Refusals assert 400, nested error.code, the located message, and that listRequests and countRequests never ran: bogus, Pending, a comma list with one bad entry named once, '', 'pending,', a nested query object; the name and arity gates still answer first. Preservation asserts the argument the service received: all six statuses as strings, known status with no rows 200 [], comma list as array, trim + dedupe, absent status leaves the filter unset, paged comma list counts total on the same array. Updated rest-server-approvals-unknown-filter.test.ts: the every-name case now sends status=pending instead of '1'. Ablation via scripts/ablation-replace.mjs replacing the readApprovalStatusFilter call with a pass-through (blob a9c203f5 -> 6b2eac2a): 8 failed / 8 passed (every refusal and the three comma-list preservation cases fail; single-status and gate-order cases hold, as they must); restored, git diff HEAD empty. @objectstack/rest vitest: 392 files, 7639 passed / 352 skipped. pnpm --filter @objectstack/rest run typecheck (tsc + check:test-typecheck): exit 0.",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at 976da77f: 58 commands, each recorded command :: exit code to a file, after a full turbo run build --filter='!@objectstack/docs' (exit 0) and a fetch of 621a4876 for the teardown self-test. All 58 exit 0. --ran: 58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN. pnpm lint (repo-wide): exit 0. PR check-runs at 976da77f: 35 success, 13 skipped, none pending or failed.",
      "dogfood_battery": "976da77f · os-verify-lock VERDICT command-exit 0 (held 29m23s) · pnpm --filter @objectstack/dogfood test · Test Files 419 passed / 3 skipped (422) · Tests 3492 passed / 36 skipped (3528) · no failures",
      "line_budget": "296 changed lines (+294 / -2) across 4 files, under the 3000 human-merge threshold",
      "files_changed": [
        "packages/rest/src/approvals-routes.ts",
        "packages/rest/src/rest-server-approvals-status-value.test.ts",
        "packages/rest/src/rest-server-approvals-unknown-filter.test.ts",
        ".changeset/29880-approvals-list-unknown-status.md"
      ],
      "deviations": [
        "Allowed set read from APPROVAL_STATUSES (the contract constant the object's select options are built from), not by a runtime metadata read of sys_approval_request: same single source, no per-poll metadata read and no failure arm. Seat agreed 12:40Z.",
        "Comma list now reaches the service as an array (a widening half beside the narrowing); without it the SDK's array call would turn from silent [] into 400. Seat agreed 12:40Z.",
        "The full workspace build before the gates ran outside os-verify-lock (the dogfood battery ran under it).",
        "main moved 18 commits after the merge (rest overlap: one unrelated test file, analytics-platform-scope-predicate-guard.test.ts). Not re-merged; head held for the contract review.",
        "Reviewer request and assignee not written (order: no MCP write; relay pr_create sent no assignees)."
      ],
      "mcp_calls": "0 — no GitHub MCP call of any kind",
      "api_writes": "3 — git push (branch), relay pr_create (#30491, 7415 bytes sent = stored), this os-dev-report via post-stamped",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: none filed · carrier: PR Acceptance notes · ui/apps/console/src/pages/system/ApprovalsInboxPage.tsx status picker offers pending/approved/rejected/recalled/returned but not cancelled, which the route and the object both have (ui lane, not touched)."
      ]
    }

    Report taken 2026-11-05T13:55Z.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

    Landed · PR #30491 · card closed

    Reading Value
    PR #30491 MERGED 2026-11-05T15:26Z, merge commit 75bbebb27; Fixes #29880 closed this card. Seat record 6660126726 on head 976da77f (BREAKING, not governed, ≤ 3000 lines; contract review PASS 6659996154; landed per pm-dispatch on the maintainer's 13:47Z word).
    main source packages/rest/src/approvals-routes.ts:365 const statusRead = readApprovalStatusFilter(q.status);
    Follow-up #30525 (the refusal message for a non-string status, and the approvals docs page missing cancelled) is unblocked now that this PR is on main.

    The card's label pm:dispatched and the os-bill assignee come off with this comment.

    Posted 2026-11-05T15:26Z.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions