Skip to content

fix(runtime)!: a mounted domain with no implementation answers 501, not a 404 that blames the route (#4093 follow-up) - #4222

Merged
os-zhuang merged 2 commits into
mainfrom
claude/dispatcher-handler-ready-1odd28
Jul 30, 2026
Merged

fix(runtime)!: a mounted domain with no implementation answers 501, not a 404 that blames the route (#4093 follow-up)#4222
os-zhuang merged 2 commits into
mainfrom
claude/dispatcher-handler-ready-1odd28

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

#4204 统一了 discovery 说的**"该装什么";这个 PR 统一"答什么码"** —— 空能力应答面上最后一块。

两个事实,一直被混在一起答

代码里其实早就有正确的区分,只是只有 mcp 一个域贯彻了:

判据 应答 现状
路由不存在 /mcp 环境未启用;/analytics 服务不可服务时 dispatcher-plugin 门控挂载、根本不注册这些路径(#4000) 宿主路由器自己的 404 不动
路由在,实现不在 所有无条件挂载的域 501 ✗ 各自为政

各自为政的三种形态

/automation/notifications 返回 { handled: false } —— 看着像"中立地让位",但它不是让位:dispatcher plugin 的唯一出口把它变成 404 ROUTE_NOT_FOUND,附带提示 "No handler matched this request. Check the API discovery endpoint for available routes."

这句话两半都是假的:handler 确实匹配了(只是背后没实现),而 discovery 正确地不列这条路由 —— 提示把人指向一个永远不会提到它的页面。运维读到这个会去找一个不存在的路由 bug。

/ui 答 503 —— 503 宣称"暂时不可用,稍后重试",但没装 MetadataPlugin 不会因为重试就装上

/ai 答 404 —— 同一类。

改法

拒绝只写一次(domains/unavailable.ts),并且携带 discovery 为该槽位报告的同一句补救指引(复用 #4204serviceUnavailableMessage)—— 于是"墙"和"discovery 条目"不可能各说各的:

POST /api/v1/automation   → 501 Install @objectstack/service-automation to enable

/ai 刻意保留本地文案:它真正的提供者 @objectstack/service-ai 作为 Cloud/EE 包在这个 workspace 之外,而 CORE_SERVICE_PROVIDERcheck:service-providers 按 workspace 包校验、因此记为 null,共享句会说"没有实现可装" —— 那是错的。"没有 workspace 包"≠"没有包" 这个缺口值得记下来;这里用覆盖参数保持诚实,不顺手放宽守卫的契约。

刻意不动

  • analytics 的挂载门 —— 路径是真的不存在
  • mcp 的 404/501 双答 —— 它就是本 PR 推广的那个模型
  • 每个域末尾handled: false —— 那是"子路径没匹配上",真 404
  • GET /ai/agents 的空列表 200 —— console 每次导航都轮询,刻意的礼遇
  • /ai503 routes not yet initialized —— 不同条件(服务在、内部状态未就绪),可能真的是暂时的

测试

9 条钉旧应答的已更新,每条最要紧的断言都保住了:automation stub 仍断言从未被调用、notifications 的 listInbox 同样、/ai/agents 仍返回空列表。route-parity 的集成断言保留原不变量(不广告 ⟹ 不提供,#3369),并写清 501 为何比它原来断言的 404 更好地表达了它。

验证

全仓 pnpm test 132/132;runtime 955;build 71/71;pnpm lint 与全部 check:* 干净 —— route-envelope 要求新增的 domains/ 文件登记为 handBuilt: 0,已登记(它只经 deps.error 作答,正是该检查自身的主张应用在"拒绝"本身上)。

🤖 Generated with Claude Code

https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B


Generated by Claude Code

…ot a 404 that blames the route (#4093 follow-up)

Two different facts were being answered with whatever each domain happened to
reach for, and only `mcp` told them apart:

  - The route is not there. `/mcp` when the server is disabled for the
    environment; `/analytics` when the service is unserveable, because
    dispatcher-plugin gates the MOUNT and never registers those paths (#4000).
    A path the server does not expose is a 404 from the host's own router.
    Unchanged — that half was already right.

  - The route is there; the implementation is not. Every unconditionally
    mounted domain. The request reached a handler with nothing to delegate to.
    That is 501, and it is what changes here.

`/automation` and `/notifications` returned `{ handled: false }`. That looks
neutral, but it is not a fall-through: the dispatcher plugin's single exit
turns it into `404 ROUTE_NOT_FOUND` with the hint "No handler matched this
request. Check the API discovery endpoint for available routes." Both halves
are false. A handler DID match — it just had nothing behind it — and discovery
correctly does not list the route, so the hint points at a page that will never
mention it. An operator reads that as a routing bug and goes looking for one
that does not exist.

`/ui` answered 503, which claims the condition is temporary. An uninstalled
MetadataPlugin does not become installed by retrying. `/ai` answered 404 for
the same mounted-but-unimplemented case.

The refusal is written once (`domains/unavailable.ts`) and carries the same
remedy sentence discovery reports for the slot, via `serviceUnavailableMessage`
from the table #4204 added — so the wall and the discovery entry cannot drift
into naming different fixes. `POST /api/v1/automation` on a stack without the
service now answers `501 Install @objectstack/service-automation to enable`.

`/ai` keeps a local message deliberately: its real provider ships outside this
workspace as a Cloud/EE package, so CORE_SERVICE_PROVIDER — verified against
workspace packages by check:service-providers — records `null` for that slot
and the shared sentence would say "nothing ships", which is wrong. That gap
between "no workspace package" and "no package" is worth knowing about; the
override keeps this change honest without widening the guard's contract here.

Deliberately unchanged: analytics's route-mount gate (a genuinely absent path);
mcp's 404-vs-501 pair, which is the model this generalizes; the `handled: false`
at the END of each domain, which means "no sub-route matched" and is a true
404; `GET /ai/agents`'s empty-list 200 (a deliberate courtesy for the console's
per-navigation poll); and `/ai`'s `503 routes not yet initialized`, a different
condition — service present, internal state unready — that may genuinely be
transient.

Tests: nine pinned the old answers and are updated to the new one, with the
part that actually mattered preserved in each — the automation stub is still
asserted NEVER CALLED, the notifications stub's `listInbox` likewise, and
`/ai/agents` still returns its empty list. route-parity's integration
assertion keeps its invariant (not advertised ⟹ not served, #3369) and now
states why 501 expresses it better than the 404 it used to assert.

Verified: full `pnpm test` 132/132 tasks; runtime 955; build 71/71; pnpm lint
and every check:* gate clean (route-envelope needed the new domains/ file
declared as handBuilt: 0 — it answers only through deps.error, which is that
check's own thesis applied to the refusal itself).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 4:29pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

18 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

The drift check flagged 18 docs — the blast radius of touching runtime — and
none of them claimed the statuses this PR changes. But one already documented
the rule for a single surface: `/graphql` is described as "route is wired but
bring-your-own service: returns 501 unless an implementation is registered".
That was the house convention all along; this PR makes it hold everywhere, so
the API docs should state it as a rule rather than as a GraphQL quirk.

Added to the Discovery section, where a reader asks the question it answers:
404 means the route is not mounted (analytics' gated mount, mcp when disabled),
501 means it is mounted with nothing behind it, the 501 body names the package
to install, and nothing answers 503 for a missing capability — that status is
for genuinely transient states like a still-booting kernel on /ready.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 16:42
@os-zhuang
os-zhuang merged commit 8a341a4 into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/dispatcher-handler-ready-1odd28 branch July 30, 2026 16:42
os-zhuang pushed a commit that referenced this pull request Jul 30, 2026
Picks up #4222, which rewrote the sibling refusal in `domains/ai.ts`
(mounted-but-unimplemented now answers 501, not 404) and re-pinned
`check-route-envelope.mjs`. It deliberately left `GET /ai/agents`'s
empty-list 200 alone, so this PR's comment block on that branch merges
without conflict — verifying it against the merged tree rather than
assuming.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZaiYNM5igzGk6JiibjShb
os-zhuang added a commit that referenced this pull request Jul 31, 2026
#4093 follow-up) (#4301)

CORE_SERVICE_PROVIDER recorded `null` for `ai` because no WORKSPACE package
provides it, so `serviceUnavailableMessage('ai')` said "No implementation ships
for the 'ai' slot". That is false — `@objectstack/service-ai` registers it —
and it is the same class of wrong answer this table was introduced to end, one
step further out: "not in this repository" was conflated with "does not exist".

How it got in is worth naming. The claim that ai's provider is a Cloud/EE
package was true, but it was asserted from a doc line and an old code comment
without checking — exactly the provenance that made `plugin-search` and
`plugin-workflow` wrong in the first place.

Verified properly now, against the cloud repository:
`packages/service-ai/src/plugin.ts` calls `ctx.registerService('ai', …)`, and
the package is `private: true`. So something ships AND there is nothing to
install — which is why `null` stays correct and why the sentence still must not
start with "Install". `search`, `workflow` and `graphql` were re-checked the
same way; nothing in either repository registers them, so their `null` and
their "nothing ships" sentence are accurate on double-sided evidence now.

`ai` gets a REMEDY_DETAIL entry — the mechanism `ui` already used for a slot the
simple sentence describes badly. Discovery (`services.ai.message`) and the /ai
501 body both report it.

This REMOVES code. `/ai`'s domain carried a local message override precisely
because the shared sentence was wrong there; correcting the table fixed the
domain AND discovery, which an override could never reach. The override and
`capabilityUnavailable`'s `message?` parameter are both gone, and the helper
documents why there is no per-call escape hatch: a slot whose sentence is wrong
needs the table corrected.

check-service-providers keeps its workspace-only rule — it cannot see another
repository, and widening it is not the fix — but it no longer repeats the
conflation in its own output, and its failure text now tells the next person
what to do in exactly this case: `null` plus a REMEDY_DETAIL sentence when
something ships that simply cannot be installed. Verified by pointing the table
at the private package and reading the error.

Also corrected four stale claims about /ai's absent-service answer: three
calling it a 404 (docs/api/client-sdk.mdx, docs/releases/v17.mdx, a comment in
packages/client's URL-conformance test), stale since #4222 made it 501; and the
discovery sample in docs/protocol/kernel/http-protocol.mdx, which quoted the
very sentence this change corrects.

Verified: full `pnpm test` 132/132 tasks; runtime 954; spec's provider suite 8;
build 71/71; pnpm lint, every check:* gate and spec's api-surface clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants