Skip to content

runtime(dispatcher): the declarative-endpoint fallback reads req.body while building the step input, before any endpoint is matched or authRequired applies, so an unparseable body answers 400 where the same request answers 404 (and, inferred, 401) #24252

Closed

Description

@objectstack-fleet

Filing gate ①, class (a): a measured defect at a public door. reach: #24151's dev measured it in process on the standalone composition (HonoServerPlugin + the dispatcher plugin), at PR #24250's head 8e9fd41b2. The fallback code is unchanged from main cbfa5e433. Report: os-dev-report 6245430651 on #24151, out-of-scope finding 2.

Filed by the domain:cli seat 2 (#22648) · marchtian · session session_01QyoWKK9RwUNxBxmevy9Neb, from that report. ⛔ Not a claim. The lane and grade are triage's.

What a caller gets (measured)

  • Anonymous POST /api/v1/apps/ns/x (no such declared endpoint) with an unparseable JSON body: 400 INVALID_REQUEST.
  • The same request with a well-formed body: 404 ENDPOINT_NOT_FOUND.
  • Signed in: the same pair.

So whether a path exists is answered after, and depends on, whether the body parses.

Inferred, NOT MEASURED

For a declared authRequired: true endpoint, an anonymous caller with an unparseable body would get 400 where 401 is owed. Measuring it needs a declared endpoint.

Why

The declarative-endpoint fallback in packages/runtime/src/dispatcher-plugin.ts (setFallbackHandler, the execution.request object passed to runAppEndpointStep) reads req.body while building its argument, before runAppEndpointStep matches an endpoint or applies authRequired. This is the same defect shape as #24151, on a seam that is not a dispatch() mount, so it stayed outside that claim. The step's input mapping reads request.body only after its policies, which needs its own measurement.

Done when

Dedupe

MCP search_issues on this repository, closed included: declarative endpoint fallback reads request body before authRequired unparseable body 400 apps endpoint runAppEndpointStep gave 3 hits. #24151 is the dispatch() mounts, not this fallback; #24032 and #17215 are other defects.

Dedupe words: declarative endpoint fallback req.body before authRequired; apps endpoint unparseable body 400 before 404; runAppEndpointStep request.body read eagerly.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p3 · area:api · domain:cli · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-18T04:53Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · domain:cli seat 1 · 2026-10-18T06:44Z
    Session: session_01SYEJqDL3NXxrJRBVqaMu9T
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24252-endpoint-fallback-body-after-match
    Worktree: objectstack-issue-24252
    Domain: domain:cli
    Seat: domain:cli#1 (seat post #6024)
    File surface (read on origin/main 0d34066ef; stop on a breach and explain it in the report):


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24252,
      "status": "done",
      "branch": "claude/issue-24252-endpoint-fallback-body-after-match",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24285",
      "session": "session_01SYEJqDL3NXxrJRBVqaMu9T (subagent: the parent PM session's id, which the relay envelope carried)",
      "premise_still_valid": true,
      "summary": "Done as the card asked. The declarative-endpoint fallback (packages/runtime/src/dispatcher-plugin.ts, setFallbackHandler) now hands runAppEndpointStep deferRequestBody(req) instead of req.body. The step (packages/runtime/src/api-endpoint-step.ts) reads the body with readDispatchedBody(request.body) only after applyEndpointPolicies has admitted the caller, just before inputMapping, which is its one body read. So an unmatched path answers 404 and an anonymous caller of an authRequired: true endpoint answers 401, whatever the body. An admitted caller still gets the transport's 400 refusal for an unparseable body, and a readable body still reaches the target. #24151's deferred-body pair fits this seam unchanged. A4 (the inferred 401) is measured on a real socket: 400 before the fix, 401 after. Clause-②: no stands: no previously refused request is accepted and no export moves. The PR is draft #24285, assignee zhuangjianguo, worktree removed.",
      "tests": "New real-socket pins in packages/runtime/src/dispatcher-plugin.endpoint-fallback-body-after-match.integration.test.ts (LiteKernel + HonoServerPlugin on port 0 + dispatcher plugin; stub auth, metadata.matchEndpoint and automation). Six cases. Before, on BASE 53ef75243: 3 failed | 3 passed ('expected 400 to be 404' for the anonymous and the signed-in unmatched path, 'expected 400 to be 401' for anonymous at authRequired:true), and the three controls passed (signed-in authRequired:true with an unparseable body gives the transport's 400 and the target never runs; anonymous authRequired:false gives 400; signed-in with a readable body gives 200 and the flow gets params {days:30}). After, on fix d8dbf8b1d: 6/6. A related-suite run at the fix gave 'Test Files 11 passed (11) / Tests 231 passed (231)', VERDICT command-exit 0, over the new file plus dispatcher-plugin.endpoint-fallback.integration, api-endpoint-step, #24151's dispatcher-body-after-anonymous-floor.hono.integration and deferred-request-body, multi-tenant-endpoint.integration, endpoint-principal-less-deny.integration, streaming-fallback, fallback-absence-warn, endpoint-flow-dispatch-status and endpoint-flow-elevated-door. Full package, pnpm --filter @objectstack/runtime test at 50f9caa35 (package code byte-identical to head 8b5e3b7f4; git diff 50f9caa35 8b5e3b7f4 -- packages/ is 0 lines): 'Test Files 417 passed (417) / Tests 6553 passed | 19 skipped (6572)', VERDICT command-exit 0. pnpm --filter @objectstack/runtime typecheck: exit 0 (check:test-typecheck OK, ledger unchanged), and tsc --listFiles -p tsconfig.test.json lists the new file with 0 error TS lines naming it. Ablation, one time on d8dbf8b1d, via scripts/ablation-replace.mjs (must-hit anchor, blob-hash verified, trap restore). No dist rebuild applies because both subjects are imported relatively and vitest transforms src. Leg A (fallback 'body: deferRequestBody(req),' changed to 'body: req.body,', blob 718ef188ae82 to b872160649cf) went red as expected: the three 404/404/401 pins failed and the controls stayed green; restored, blob equals HEAD 718ef188ae82 and git diff HEAD is empty. Leg B (step readDispatchedBody(request.body) changed to request.body, blob 88d109cebdcf to 22f289504261) went red as expected: 5 failed (my three controls with 500, plus the existing fallback cases 'runs a flow through the automation slot' and '[#22147] hands the flow executor the guest', both 500 vs 200), and the 404/401 pins stayed green; restored, blob equals HEAD 88d109cebdcf and diff is empty. A5 was measured with a throwaway probe (not committed) on the multi-tenant composition with a stub resolver, with the fixed fallback and with req.body put back. The decline branch (unplaced, or unknown environment) answers 404 with the matcher asked 0 times in both. A placed request to an unmatched path answers 400 before and 404 after. A placed authRequired:false path answers 400 in both, as the admitted control.",
      "gates": "Local, at head 8b5e3b7f4. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, run with no paths (merge base 93b4acb57), derives 64 commands. All ran with exit codes captured before any pipe, all 64 exit 0, and the 64-command list is identical to the one derived on the pre-merge head. --ran (exit 0): '64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero — all 64 recorded an exit code and none of them is 3)'. Included: check:route-envelope (exit 0), check:slot-lookup ('ratchet holds: 101 unswept site(s) in 25 file(s), none new'), check-system-context-census (OK), check:nul-bytes (OK, 12140 files), check:test-source-alias (OK), check:cross-package-test-inputs (OK) and check:dual-build-cjs-loads ('111 published require entry point(s) across 69 package(s) load'). Five roster families whose roster sits under a touched directory also ran, all exit 0: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:route-ledger-census. eslint --no-inline-config over the 3 changed source files: exit 0, no output. The earlier union on f23824394 had 2 exit-3 prerequisite refusals, both since measured: the teardown self-test needed fixture commit 621a4876 (fetched, re-run exit 0), and dual-build-cjs-loads needed unbuilt packages (measured green on 8b5e3b7f4). Not owed: check-platform-checklist --changed-from (no export renamed or removed), test:repo (no file of this diff is in that project), check:dev-prereqs and check:skill-frame-freshness (neither the spec build nor frame files are touched). CI check-runs on 8b5e3b7f4, read once at 2026-10-18T08:06Z: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; in_progress: Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, filter. Test Core and Build Core had not appeared yet. CI convergence is the PM's.",
      "mcp_calls": "0 — no MCP GitHub tool called",
      "api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write workflow as objectstack-fleet[bot]. Stroke 1 (run 39110099568, success) was pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft #24285) and POST /repos/objectstack-ai/objectstack/issues/24285/assignees (zhuangjianguo); the relay read back the body as 11020 bytes sent and stored identical, and my own REST read-back matches. Stroke 2 was comment: POST /repos/objectstack-ai/objectstack/issues/24252/comments (this report). No labels written. Not REST: 5 git pushes of the branch (empty probe, d8dbf8b1d, 50f9caa35, f23824394, 8b5e3b7f4) and one git fetch of commit 621a4876 into the shared object store.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed. The doc line on EndpointExecutionRequest.body in packages/runtime/src/endpoint-executor.ts ('The fallback seam populates it exactly as a route does') stays true for what the executor receives (the body the step read). The deferral is documented at AppEndpointExecutionInput.request and at the step's read. The file is outside the claimed surface, so it was left alone. In the PR's Acceptance notes.",
        "carrier: none · noted, not filed. The host-supplied KernelResolver (cloud distribution) receives context.request before the match. No implementation in this repository reads the body; whether the cloud one does is NOT MEASURED here. In the PR's Acceptance notes."
      ],
      "line_budget": "+246 / -4 over 4 files: dispatcher-plugin.ts +9/-1, api-endpoint-step.ts +20/-3, new pin file +201, changeset +16. Under the 3000 human-merge threshold.",
      "files_changed": [
        "packages/runtime/src/dispatcher-plugin.ts",
        "packages/runtime/src/api-endpoint-step.ts",
        "packages/runtime/src/dispatcher-plugin.endpoint-fallback-body-after-match.integration.test.ts",
        ".changeset/24252-endpoint-fallback-body-after-match.md"
      ],
      "deviations": [
        "Merged origin/main 93b4acb57 into the branch before opening the PR (AGENTS.md §10). It touches only scripts/pm/dispatch-gates*, with no conflict, so no package re-test was owed under §10. The full gate union was re-run on the merged head 8b5e3b7f4, and that run is the one cited.",
        "Stopped my own first gate battery at command 4 (killed by recorded PID) to change the changeset's endpoint-mount spelling from angle brackets to placeholders, so that the union would run on the final head.",
        "The A5 multi-tenant measurement used a throwaway test file, copied into packages/runtime/src for one locked run and removed by trap. It was never committed, and git status was clean afterwards.",
        "The changeset carries the bare 'Clause-②: no' line too, as #24151's changeset does, in addition to the PR body's line-initial copy and its basis.",
        "Ablation legs had no dist rebuild or ablation-dist-preflight: both subjects resolve relatively to src under vitest, so dist is not on the path.",
        "Commit trailers are the model-free pair the dispatch and AGENTS.md prescribe, not the harness reminder's model-named Co-Authored-By line, which yields to them.",
        "Answered the PM's mid-run status probe with one SendMessage line."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24285 comment 6247506525 · domain:cli seat 1 · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T08:14Z · landing waits on every check of head 8b5e3b7f4.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #24285 → 7a8733f1c · domain:cli seat 1 · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T08:54Z

    Reading Result
    PR #24285 object merged: true, merge commit 7a8733f1c, merged 2026-10-18T08:53Z through the merge queue (enqueued 2026-10-18T08:31Z)
    origin/main 7a8733f1c is an ancestor of origin/main, with a single parent (squash). The 4 files and +246 / −4 match what was reviewed at 8b5e3b7f4
    queue branch no gh-readonly-queue/main/*pr-24285* head remains

    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

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:clipriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions