Skip to content

actions: an api action called with no recordId forwards an unresolved {id} path and answers 404 ENDPOINT_NOT_FOUND instead of naming the missing record id #28932

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbZpHPpovxXPremPLtv

立卡门: ① 产品缺陷,具名落点 + 复现(reach: 公开入口 HTTP POST /api/v1/actions/:object/:action 实测一次错误结果)
业务理由: 通过 REST 或 MCP 调用记录型 api 动作的集成方,漏传 recordId 时得到「端点不存在」,会以为路由坏了或版本不对,而不是自己少传了参数。不修,排障成本一直落在调用方身上。
动手的读者: domain:server 席,packages/runtime/src/action-execution.ts 的 buildApiActionRequest({field} 令牌未替换时提前 400 拒收),附 action door HTTP pin。
查重: repo is:issue ENDPOINT_NOT_FOUND recordId 21 命中、buildApiActionRequest 3(#27738、#27611、#27610,都不是本现象)、api action unresolved token 105(均含已关)。

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

What happens

POST /api/v1/actions/sys_approval_request/approval_approve with {"params":{}} (no recordId), or with "recordId":"", answers 404 ENDPOINT_NOT_FOUND "Not found". The caller is told the endpoint does not exist, when the real problem is the missing record id the action's target path needs. The same call with a real request id answers 200 (the #28657 fix holds), and with an unknown id answers 404 REQUEST_NOT_FOUND.

Measured at framework 7fa9873f (main, 2026-11-02) on a stock showcase boot, as a non-admin pending approver (the #28657 persona).

Reproduction

  1. Boot the stock showcase and mint the approvals: a non-admin approver's decision from the request page answers 404 RECORD_NOT_FOUND — the action door loads sys_approval_request in the caller's data scope #28657 persona X (invite-member + sign-up; positions contributor and finance; X creates a showcase_invoice and PATCHes it to status: sent with issued_on).
  2. As X: POST /api/v1/actions/sys_approval_request/approval_approve {"params":{}}.
  3. Expected: 400 naming the missing recordId (the action declares a record-anchored target).
  4. Actual: 404 {"code":"ENDPOINT_NOT_FOUND","message":"Not found"}. Same with "recordId":"".

Mechanism (read at 7fa9873, a guess)

buildApiActionRequest in packages/runtime/src/action-execution.ts substitutes {field} tokens only when a subject row is present, and its unresolved-token guard checks only ${…} and {{ forms, so the forward goes out with {id} literally in the path and the router answers 404.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: priority:p2 · domain:server · pm:queue · 2026-11-02T05:06Z

    ⛔ Not a claim.

    • Read fresh. The card was observed beside approvals.account-app-entry, but the defect is on the generic action door, not on that capability. buildApiActionRequest (packages/runtime/src/action-execution.ts) forwards a literal {id} when no recordId is given, so the door answers 404 ENDPOINT_NOT_FOUND instead of a 400 naming the missing id. That is a user-visible behaviour defect, so it is p2 in domain:server, and with the location and repro named it goes to pm:queue.

    Generated by Claude Code

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

    Claim: PM loop round 1 · card thread · 2026-11-02T05:20Z
    Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat); the card thread is session_01FEkQ5nNCtq276yJtjqKq8B (a new card thread, fresh from main 62b3ec36)
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-4yqy8a (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#1
    Ruling-ref: triage#1 graded the card p2 (05:06Z): a generic action-door defect. Route (the dispatching seat's call): buildApiActionRequest refuses a target path whose {field} token is unresolved with 400 naming recordId, before any forward. Dispatched in the seat-1 split of the server queue under 「队列进了很多任务,抓紧派发」 (Leehom, 2026-11-02T05:15Z) and 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z).
    File surface: buildApiActionRequest and its unresolved-token guard in packages/runtime/src/action-execution.ts; their tests and changesets. ⛔ No packages/spec/**, no new error code, no new gate.
    Container & model: S, mode:thread, model and effort as the thread was opened
    Clause-②: no (expected: a call that was already refused 404 is refused 400 naming the missing id; the accept set does not change; the dev measures)
    Metadata-protocol: per AGENTS.md, measured by the dev (likely none)
    Responsibility: the code is the api action forward of @objectstack/runtime · the platform path is a record-anchored api action called with no recordId answering ENDPOINT_NOT_FOUND · reached by every REST or MCP caller of POST /api/v1/actions/:object/:action
    Thread-read: none
    Premise: holds on main 7fa9873f (the card's repro)
    Serial constraints cleared: #28657 landed. #28943 (seat 1, another thread) edits the credential table in the same file, so merge origin/main once if it lands first.
    Resume-from: none — fresh from origin/main

    State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-project-manager · session_01FEkQ5nNCtq276yJtjqKq8B

    os-dev-report
    {
    "issue": 28932,
    "status": "done",
    "branch": "claude/project-thread-4yqy8a",
    "pr": "#29005",
    "session": "session_01FEkQ5nNCtq276yJtjqKq8B",
    "premise_still_valid": true,
    "summary": "buildApiActionRequest (packages/runtime/src/action-execution.ts) now refuses a target record token it cannot fill before any forward: an unresolved ${ctx.recordId} (an empty recordId counts as none) and any {field} token left in the path answer 400 VALIDATION_ERROR naming recordId when no record was named, otherwise the field the row lacks. Both doors (REST /actions and MCP run_action) share it; no new error code, no spec change. Step-1 measurement: every record-token api target in objectstack names {id} alone (9 sys_approval_request actions, 2 sys_api_key, 1 dev console page); 2 datasource actions use ${ctx.recordId}; examples/ and hotcrm have none.",
    "tests": "Head f71c6e2, base f7c545a. Unit packages/runtime/src/domains/api-action-forward.test.ts: 73/73 green; with action-execution.ts from base 4 failed / 69 passed (the 4 refusal tests; controls green). Real stack packages/verify/src/action-door-api-forward-denied-subject.test.ts: 5/5 green; with runtime dist rebuilt from base 1 failed / 4 passed, reading 404 ENDPOINT_NOT_FOUND 'Not found' (the card's repro). Ablation from committed HEAD via scripts/ablation-replace.mjs (both guard lines, anchor 1 to 0 each): unit 4 failed / 69 passed; verify after runtime build + ablation-dist-preflight 'marker present in 2 built files': 1 failed / 4 passed (400 FILTER_TOKEN_UNKNOWN, not VALIDATION_ERROR). Restore: git diff HEAD 0 bytes, blob 12811782c09a equal to HEAD, rebuilt, preflight --absent 'marker absent from all 12 built files', tree clean. Full pnpm --filter @objectstack/runtime test: 513 files, 8272 passed, 19 skipped (lock VERDICT command-exit 0). runtime typecheck exit 0 (test-typecheck ledger held 27/188/68); verify typecheck exit 0; eslint --no-inline-config packages/runtime packages/verify exit 0.",
    "gates": "dispatch-gates --commands at f71c6e2: 61 commands, each run with exit captured before any pipe (record /mnt/project-files/pm-orders/server/28932-gates-ran.txt). 60 exit 0. check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs a full repo build; only the verify closure was built) = NOT MEASURED, left to CI. check-plugin-teardown-shape --self-test exit 3 on the shallow clone, exit 0 (48 cases) after git fetch --depth=1 of the commit it reads; exit 0 recorded. dispatch-gates --ran: 61 derived, 60 run, 1 NOT-MEASURED, 0 UNRUN. CI check-runs on the PR: in_progress at report time.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0",
    "api_writes": "2 relay writes plus 1 git push: git push of claude/project-thread-4yqy8a; fleet-write pr_create (run 41397679904, PR #29005, no assignees, no labels); this os-dev-report comment via post-stamped",
    "open_questions": [],
    "out_of_scope_findings": [
    "none — the one adjacent shape (a scope-less ${name} or a double-brace name target whose {name} matched no row field, forwarded blanked on main) is closed by the same guard and noted in the PR's Acceptance notes; no such target exists in objectstack, examples/ or hotcrm"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

    Landing record · #28932 · PR #29005

    PR #29005 merged by the merge queue as 2bfdc4261e on main (Fixes #28932; Clause-②: no, with no contract review owed; ACCEPT 6570373416). The card closed on the merge.

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-29005-*' 0 refs
    origin/main by content git diff --quiet 2bfdc4261e origin/main -- the PR's 4 files all 4 identical
    Ancestry git merge-base --is-ancestor 2bfdc4261e origin/main holds
    • Landed: an api action whose target names its record is refused 400 VALIDATION_ERROR naming recordId (or the field the row lacks) before any forward, on the REST action door and MCP run_action, instead of being forwarded to a path that answers 404 ENDPOINT_NOT_FOUND. @objectstack/runtime patch, Metadata-protocol: none.

    Release: branch claude/project-thread-4yqy8a merged; nothing unpushed. pm:dispatched removed and the card unassigned.


    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

No one assigned

    Labels

    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions