Skip to content

fix(rest): 预期 4xx 不再打「[REST] Unhandled error」+ 栈 —— 两族路由收敛到同一道门 (#4886) - #5394

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-4886-rest-expected-404-logging
Aug 5, 2026
Merged

fix(rest): 预期 4xx 不再打「[REST] Unhandled error」+ 栈 —— 两族路由收敛到同一道门 (#4886)#5394
baozhoutao merged 1 commit into
mainfrom
claude/issue-4886-rest-expected-404-logging

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #4886

问题

打开 Studio 任意设计器面板,服务端日志被栈刷屏。设计器每块面板都会探测
GET /meta/:type/:name?state=draft 来决定是否显示「未保存草稿」状态,而
「没有草稿」是压倒性常见的答案 —— 对每一个当前无人编辑的构件都成立。
getMetaItem 抛出结构化的 { code: 'NO_DRAFT', status: 404 },客户端拿到
干净的 404 并正常处理,线上行为一直是对的,错的只是日志:一次浏览会话
里 45 条栈。这既埋掉了真错误(真 500 就是这样被淹没的),也误报了严重度 ——
「Unhandled error」+ 栈在告诉运维出事了,而其实什么事都没有。

前提复核(按 Prime Directive 6)

issue 基线是 0e96e46,rest-server.ts 此后被 #5362 触及,行号已漂移。
origin/main(ccba1bb33)上重新数过:29 处无条件
logError("[REST] Unhandled error:", error) 依然成立
,前提有效;只是热点
draft-read 路径从 issue 说的 ~L3923 漂到了 ~L3762。

修法

data 族其实早就有这个概念(isExpectedDataStatus / isExpectedQueryRejection),
但散成了 12 处、4 种不同写法;而 isExpectedQueryRejection 的 docblock
本身就记着上一轮同型漂移(filter/sort 两个 code 上线时没加进列表,于是它们
产生的每一次拒绝也都被当成 unhandled 打了出来)。所以这次不是给 metadata 族
补一个判断,而是让两族收敛到同一道门:

新增 职责
resolveErrorResponse() sendError 里拆出来:算出「客户端将要收到的 status/body」但不发送。日志判断因此读的是响应本身,而不是另起一份会漂移的判断
isExpectedRouteError() 唯一谓词 = 预期生命周期状态(403/404/409/502/503)∪ 客户端造成的 400 查询拒绝词汇 ∪ VALIDATION_FAILED
handleRouteError() 路由 catch 的单一入口:算一次 → 只在真故障时打日志 → 发送
logUnexpectedRouteError() 只给判决,供那些必须保留自己响应写法的 CRUD catch 使用(其中一处会把 400 改写成 404 发出去)

改完之后 isExpectedDataStatusisExpectedQueryRejection 只剩
isExpectedRouteError 一个调用者
—— 这是「两族不会再漂移」的结构性保证,
而不是靠注释约束。

谓词刻意不是「任何 4xx 都算预期」:mapDataError 会把它完全不认识的错误
降级成一个无 code 的 400,而真正的 handler bug(比如 TypeError)正落在
这一桶里 —— 把它静音就成了本 issue 的镜像缺陷。

线上行为

不改任何 wire response,逐行核对过 diff 里每一条发响应的语句(含那处
400→404 改写),status 和 body 都与改前逐字节一致;本 PR 只决定「日志那行打不打」。

除 metadata 族外,有两处运维可见的日志变化,如实列出:

  • 跨对象事务批量路由原先只按 status >= 500 判断,顺带吞掉了那个无 code 的
    400
    —— 批量事务里的 handler TypeError 过去是凭空消失的,现在会打出来;
  • updateMany / deleteMany / clone / 全局搜索 / public-form 路由不再把正常的
    404、403 和查询拒绝当成 unhandled 打。

测试

新增 packages/rest/src/rest-expected-error-logging.test.ts(8 例),两个方向
都钉住,且两个方向并不对称,这点在测试文件顶部写明了:

  • 静音向(5 例):把修复回退成无条件打印 → 全红(预期方向)
  • 响亮向(3 例):回退后依然全绿 —— 它们防的不是回退,而是反向过度
    修正
    。把谓词放宽成「任何 4xx 都算预期」验证 → 这 3 例转红

实测:

✓ NO_DRAFT from the designer draft probe logs NOTHING and still 404s cleanly
✓ stays quiet across the sibling expected statuses, not just 404
✓ a VALIDATION_FAILED 400 is also expected
✓ a 500 still logs the full error object
✓ an UNRECOGNISED error (handler bug) stays loud even though it maps to 400
✓ the same structured 404 is silent on a metadata route AND a data route
✓ the same unrecognised fault is loud on a metadata route AND a data route
✓ a client-caused query rejection is silent on the data list route
Test Files 1 passed (1) | Tests 8 passed (8)

回退验证(无条件打印):    Tests 5 failed | 3 passed (8)
过度修正验证(任何 4xx):  Tests 3 failed | 5 passed (8)

包级全量:

pnpm --filter @objectstack/rest test   →  Test Files 43 passed (43) | Tests 652 passed (652)

类型:@objectstack/resttypecheck 脚本(在
check-type-check-coverage.mjs 的 DEBT/TEST_DEBT 台账里),故直接跑
tsc --noEmit 对比基线 —— 改前改后同为 2 条既有报错,均在无关的
package-routes.ts,新增 0 条

@objectstack/runtime 全量对比(改前 / 改后完全一致,均为容器内网络
fetch failed 导致的既有失败,与本改动无关):

改前(干净 origin/main): Test Files 21 failed | 70 passed (91) | Tests 8 failed | 1089 passed
改后:                   Test Files 21 failed | 70 passed (91) | Tests 8 failed | 1089 passed

node scripts/check-nul-bytes.mjs OK;并按字节纪律额外自扫了控制字符,干净。

消费半径巡检

sendError / isExpectedDataStatus / isExpectedQueryRejection /
handleRouteError 均为 rest-server.ts 模块私有(该文件只导出
mapDataErrorzodIssuesToFields 等,行为未变),无跨包消费者。
package-routes.ts 里的 sendError@objectstack/types 的同名不同物。
另外巡了 external-datasource-routes / storage-routes / settings-routes /
share-link-routes / admin-routes,这些路由面根本不打错误日志,不存在同类缺陷。

已加 changeset(@objectstack/rest: patch)。


Generated by Claude Code

…t "[REST] Unhandled error" (#4886)

The metadata routes had 29 catch blocks logging every thrown error
unconditionally. Studio's designer probes GET /meta/:type/:name?state=draft on
every panel, and "no draft exists" is the overwhelmingly common answer, so the
structured { code: 'NO_DRAFT', status: 404 } printed a full stack trace per
panel -- 45 in one browsing session, burying real errors and misreporting
severity. The wire answer was already a correct, clean 404.

The data routes already consulted isExpectedDataStatus / isExpectedQueryRejection
but in four open-coded spellings across 12 sites, and the latter's docblock
records an earlier lap of the same drift (the filter and sort codes shipped
without joining the list).

Both families now decide through one predicate behind one door:

- resolveErrorResponse() -- split out of sendError so the logging decision reads
  the exact status/body the client gets, not a second opinion that can drift
- isExpectedRouteError() -- the union of the three conditions: expected
  lifecycle statuses, the client-caused 400 query-rejection vocabulary, and
  VALIDATION_FAILED. Deliberately NOT "any 4xx": mapDataError degrades an
  unrecognised error to an un-coded 400, which is where a real handler bug
  lands, so that stays loud.
- handleRouteError() -- resolve once, log only genuine faults, then send
- logUnexpectedRouteError() -- the verdict alone, for the CRUD catches that
  must keep their own responder (one rewrites 400 to 404 on the wire)

isExpectedDataStatus and isExpectedQueryRejection now have no other callers, so
the families cannot drift apart again.

No wire responses change; this only decides whether the log line is printed.
Two operator-visible log deltas beyond the metadata fix: the transactional batch
route judged on status >= 500 alone and so swallowed the un-coded 400 (a handler
TypeError inside a batch transaction used to vanish, and now prints), and
updateMany/deleteMany/clone/global-search/public-form stop logging normal 404s,
403s and query rejections.

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

vercel Bot commented Aug 5, 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 Aug 5, 2026 5:15am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)
  • content/docs/releases/implementation-status.mdx (via @objectstack/rest)
  • content/docs/releases/v12.mdx (via @objectstack/rest)
  • content/docs/releases/v17.mdx (via @objectstack/rest)

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.

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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Metadata routes log every expected 404 as "[REST] Unhandled error" with a stack trace — Studio's draft probe produced 45 in one browsing session

2 participants