fix(rest): 预期 4xx 不再打「[REST] Unhandled error」+ 栈 —— 两族路由收敛到同一道门 (#4886) - #5394
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 5, 2026 05:17
baozhoutao
enabled auto-merge
August 5, 2026 05:17
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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()VALIDATION_FAILEDhandleRouteError()logUnexpectedRouteError()改完之后
isExpectedDataStatus和isExpectedQueryRejection只剩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 例),两个方向都钉住,且两个方向并不对称,这点在测试文件顶部写明了:
修正。把谓词放宽成「任何 4xx 都算预期」验证 → 这 3 例转红
实测:
包级全量:
类型:
@objectstack/rest无typecheck脚本(在check-type-check-coverage.mjs的 DEBT/TEST_DEBT 台账里),故直接跑tsc --noEmit对比基线 —— 改前改后同为 2 条既有报错,均在无关的package-routes.ts,新增 0 条。@objectstack/runtime全量对比(改前 / 改后完全一致,均为容器内网络fetch failed导致的既有失败,与本改动无关):node scripts/check-nul-bytes.mjsOK;并按字节纪律额外自扫了控制字符,干净。消费半径巡检
sendError/isExpectedDataStatus/isExpectedQueryRejection/handleRouteError均为rest-server.ts模块私有(该文件只导出mapDataError、zodIssuesToFields等,行为未变),无跨包消费者。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