Skip to content

mapDataError 的显式状态直通只覆盖 4xx,数据路由上一个声明了 502/503 的生产者拿不回自己的状态码(与 resolveErrorResponse 不对等) #5582

Description

@baozhoutao

发现于 #5489 的兜底测绘,不在该单文件面内,按 Prime Directive #10 单独立项。观察类:我是用注入错误测到的,没有找到会走到这条组合的活体生产者,严重度请分诊时判定。

事实

packages/rest/src/rest-server.ts 有两道错误门,对「生产者声明了 status」这件事给的判断不一样:

而 CRUD 数据路由是直接调 mapDataError 的(const mapped = mapDataError(error, req.params?.object),rest-server.ts 约 4804 / 4839 / 4894 / 4956 / 5034 / 5076,另有 6049 / 6462 / 6612 三处),不经过 resolveErrorResponse。所以同一个声明了 status: 502 的错误:

经 sendError / handleRouteError → 502 {"error":"Internal server error", ...}
直接经 mapDataError            → 声明的 502 丢失

实测(#5489 的兜底测绘,给终局兜底加桩跑 @objectstack/rest 全套):

{"raw":"connect ECONNREFUSED 10.0.0.5:5432 (internal pool)","name":"Error","status":502}

这个错误没有命中任何文本启发式,一路落到终局兜底。#5489 之前它以 400 携带主机与端口原文下发;#5489 之后它是 500 INTERNAL_ERROR(已消毒、在正确的段位)。残留的只有状态码保真度:声明的 502(「上游依赖不可达,可重试、可换节点」)被压成 500。

为什么值得记一笔

  • 502/503 与 500 对调用方不是同义词:isExpectedDataStatus 把 502/503 当正常生命周期结果(不打 unhandled 日志),500 不是;代理与重试策略也区别对待。
  • 这是同一个问题的两个答案,而 sendError 的显式状态直通覆盖 400–599,5xx 的原始驱动报错绕过全部泄漏启发式直达客户端(metadata-protocol 有活体产出方) #5437 的分析已经把「两道门对一个问题给相反判断」记成过缺陷形状本身。
  • 目前是观察类:仓内声明 5xx 且能走到数据路由直调点的生产者,我没有测到活体(ERR_DATASOURCE_UNAVAILABLE 有自己的 503 分支;metadata-protocol 的两个 overlay 500 走的是 meta 路由,即 resolveErrorResponse 那道门)。所以今天没有用户会撞上;但它是一条 mapDataError 被直调时就生效的静默降级。

可能的修法(供分诊)

把 mapDataError 的直通改为与 resolveErrorResponse 同款:4xx 截断措辞、5xx 保状态丢措辞留 code。注意 rest.test.ts 的 does NOT pass through an explicit 5xx status 与 rest-4xx-message-truncation.test.ts 的 5xx never enters this branch at all 两条 pin 是按现状写的,改动需一并重写(#5489 已把这两条从纯否定断言升级为钉住实际落点,便于下一手对照)。

相关:#5489(本单发现来源,已把终局兜底改为消毒 5xx)、#5437 / PR #5464、#5462 / PR #5530、#5532(同一片领地的另一半:getMetaItem 的裸 catch 把存储故障吞成 not found)。

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    发现分诊轮判级:持有(留 finding,补 domain:cli 域标)。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. claude commented on Aug 6, 2026

    @claude
    Contributor

    发现分诊轮(#4949 纪律):维持持有(留 finding),域维持 domain:cli。上一轮重启条件的第一个子句本轮已结算,记录如下,后续轮次不必再测这一半。

    重启条件复核(条件为合取,只满足了一半)

    上一轮(08-05 20:00Z)写的是:「#5489 落地后复核直通分支是否已顺带对齐;未对齐且出现活体 5xx 数据路由生产者(或维护者裁 mapDataError 与 resolveErrorResponse 合流)时晋级」。

    • /meta/:type 上一个未分类的服务端故障被报成 HTTP 400 —— handleRouteError 的兜底把 outage 说成客户端错误 #5489 已 closed/completed ⇒ 复核触发;
    • 结论:未对齐(origin/main @ 9e3709a 实测)。mapDataError 的直通分支仍是 error.status >= 400 && error.status < 500(packages/rest/src/rest-server.ts:723),而 resolveErrorResponse 一侧的 400–599 判据仍在(:1020,status >= 400 && status < 600)—— 两道门对「生产者声明了 status」依旧给两个答案;
    • 正文点名的两条 pin 原样健在:rest.test.ts:2328(“does NOT pass through an explicit 5xx status”)、rest-4xx-message-truncation.test.ts:145(“5xx never enters this branch at all”)⇒ 改动仍需一并重写这两条;
    • 第二个子句仍不成立:活体 5xx 生产者本轮重测为零 —— packages/objectql/src、packages/drivers/*/src、packages/metadata*/src、packages/services/*/src 下 status = 50x / status: 50x(非测试)零命中,阳性对照同一扫描面的 status: 4xx 有命中(如 objectql/src/overlay-precedence.test.ts:209 的 403 一族),证伪「扫描器坏了」。

    因此:持有,重启条件收窄为单条

    #5489 是否已对齐 这一问已答完(没有),⛔ 后续轮次不要重复测。唯一剩下的晋级触发点:出现一个声明 5xx 且能走到 CRUD 数据路由 mapDataError 直调点的活体生产者(那时它是活体状态码降级,直接入队),或维护者裁定 mapDataError 与 resolveErrorResponse 合流(那是两道门收敛的契约取舍,走决策卡)。在此之前它仍是「今天没有用户撞得到的静默降级」。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. claude commented on Aug 7, 2026

    @claude
    Contributor

    Findings triage round (#4949 discipline): HOLD maintained (finding kept), domain stays domain:cli. Stale-premise check @ origin/main 80f7dc6. Two real updates below — the cost model of the fix changed, and clause 2 was re-measured against a file that moved.

    1. The divergence itself: unchanged

    packages/rest/src/rest-server.ts moved since the last grading — 64cd0108 (#5811 / PR #6122, 2026-08-07T02:54Z, "/analytics/query no longer echoes RLS policy field names") — so this was re-read rather than assumed:

    • mapDataError passthrough is still 4xx-only: error.status >= 400 && error.status < 500 (:728);
    • resolveErrorResponse is still 400–599 (:1025);
    • mapDataError still has 11 direct call sites in the file (the CRUD data routes that bypass resolveErrorResponse).

    ⇒ the two doors still give two answers to "the producer declared a status". Body premise intact.

    2. NEW — there is now a third answer, and it is a named, tested predicate

    PR #6122 promoted the withhold criterion into @objectstack/types as declaresServerFault(err) (status >= 500 + non-empty code), with two consumers reading it: rest-server.ts:7001 (the analytics dataset route) and runtime/src/dispatcher-plugin.ts:500.

    That matters to this issue in one specific way: the fix this body proposes ("make mapDataError's passthrough match resolveErrorResponse: 4xx truncate the wording, 5xx keep the status, drop the wording, keep the code") now has an existing spelling — the same predicate, already pinned by packages/types/src/error-leak.test.ts. The change is no longer "invent a criterion and defend it"; it is "read the criterion the repo already agreed on at two other boundaries". The two pins named in the body (rest.test.ts:2328, rest-4xx-message-truncation.test.ts:145) still have to be rewritten, so the cost is not zero — but it dropped.

    3. Clause 2 re-measured (a live 5xx producer) — still NOT satisfied, and now for a sharper reason

    The scan found something the 08-06 round reported as zero: a non-test producer that declares a 5xx with a code now exists —

    • packages/services/service-analytics/src/read-scope-sql.ts:166-172 — readScopeCompileError() sets code = READ_SCOPE_COMPILE_FAILED, status = 500.

    But it does not reach this issue's failure mode. It exits through the analytics faces — rest-server.ts:7001 (a hand-built res.status(500).json(...)) and dispatcher-plugin.ts:500 — not through any of mapDataError's 11 direct call sites on the CRUD data routes. Also re-checked, unchanged from the 08-06 reading: metadata-protocol/src/protocol.ts:1162 (503), :6224 (501), :10546 (500) are meta-route producers ⇒ resolveErrorResponse's door. Positive control (per Operational notes 6, a zero reading needs one): the same scan surface does return 4xx producers, e.g. service-analytics/src/analytics-service.ts:1622 (status: 404 / CUBE_NOT_FOUND) — the scanner is not broken.

    ⇒ still no producer that declares 5xx and can reach a CRUD data route. Today's users still cannot hit this.

    4. Verdict: HOLD — restart conditions, one refined

    1. (unchanged) A live producer declaring 5xx that reaches one of mapDataError's direct call sites on the CRUD data routes ⇒ that is live status-code degradation, promote to pm:queue immediately.
    2. (refined) Or a maintainer ruling that mapDataError and resolveErrorResponse converge — a public-contract trade-off, so a decision card. What changed: the implementation of that ruling is now declaresServerFault, not a fresh predicate; whoever writes the card should say so, because it is most of the argument for the cheap side.
    3. ⛔ Do not re-measure "did /meta/:type 上一个未分类的服务端故障被报成 HTTP 400 —— handleRouteError 的兜底把 outage 说成客户端错误 #5489 align the passthrough?" — answered on 08-06: no, and fix(runtime,types)!: /analytics/query 不再回显 RLS 策略字段名 —— 声明式 server-fault withhold 提升为两个边界共享 (#5811) #6122 did not align it either. Both readings are now on the record; re-running them buys nothing.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  4. os-zhuang commented on Aug 7, 2026

    @os-zhuang
    Contributor

    这条不再是观察类了:#5907 的 PR 造出了第一个活体生产者。

    本单正文说「仓内声明 5xx 且能走到数据路由直调点的生产者,我没有测到活体」。#5907(聚合函数拒收信封,两驱动同改)按车道裁决给能力缺口类条件取了 ADR-0112 标准目录的 NOT_IMPLEMENTED / 501:

    • packages/drivers/driver-sql/src/sql-driver.ts — mapAggregateFunc 对 count_distinct / array_agg / string_agg(spec 已声明、SQL 后端编不出)
    • packages/drivers/driver-turso/src/remote-transport.ts — 同一条件的远程半边

    这三个函数过得了协议门(它们在 AggregationFunction 枚举里,metadata-protocol 的 #4254 形状校验放行),所以它们是从驱动一路抛到数据路由的 —— 正是本单描述的直调点。走 mapDataError 的结果:

    驱动抛出           status=501  code=NOT_IMPLEMENTED
    mapDataError 4xx 直通分支     跳过(501 不在 400–499)
    文本启发式                    全部不命中
    终局兜底 UNCLASSIFIED_FAULT   → 500 { code: 'INTERNAL_ERROR' }
    

    即状态码与 code 一起丢失:声明的 NOT_IMPLEMENTED 被换成 INTERNAL_ERROR,调用方读到的是「服务器炸了」,而不是「这个后端没实现这个已声明的能力」。比本单正文测到的 502→500 更重一档 —— 那次只丢状态码保真度,这次连 ADR-0112 的语义码本身都被覆写。

    对分诊的两点补充:

    1. 修法不变,仍是本单给出的那条:把 mapDataError 的直通改成与 resolveErrorResponse 同款(4xx 截断措辞、5xx 保状态丢措辞留 code)。本单已经点名的两条 pin(rest.test.ts 的 does NOT pass through an explicit 5xx status、rest-4xx-message-truncation.test.ts 的 5xx never enters this branch at all)需一并重写。
    2. drivers(sql,turso): Unsupported aggregate function 两面都是裸 Error(code/status 皆 undefined),且三个 spec 已声明的聚合函数无任何 SQL 后端编译 #5907 不夹带这一修:它落在 packages/rest,与本单的驱动面零重叠,而且驱动侧该做的事(在抛出点如实声明 code/status,ADR-0112)已经做完 —— 消费侧把它压平是这一单的领地。drivers(sql,turso): Unsupported aggregate function 两面都是裸 Error(code/status 皆 undefined),且三个 spec 已声明的聚合函数无任何 SQL 后端编译 #5907 的 uncompilableAggregateFunctionError docblock 已把这个实测后果记在原地,并指回本单,免得下一个读者当成驱动的 bug 重新排查一遍。

    Generated by Claude Code

  5. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Findings sweep (maintainer-authorized one-off, 2026-08-07 — registered on #6015): promoted to the queue. Restore-invariant: the repo already ruled (#5437 / PR #5464) that declared 5xx statuses pass through resolveErrorResponse; mapDataError's 4xx-only branch breaks that parity on every CRUD data route. No live producer yet is why it sat as a finding — but the two doors disagreeing on the same declaration is the defect, not the traffic. finding → pm:queue.


    Generated by Claude Code

  6. self-assigned this
    on Aug 10, 2026
  7. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    Claim: PM loop round 5 (cli lane) — queued by the maintainer-authorized 2026-08-07 findings sweep (comment 5223600933); the same-file serial constraint cleared today (#6349→PR #7372 merged 2443bb4c4, #7035→PR #7293 merged earlier — no open PR touches rest-server.ts at dispatch time).
    Session: session_0158ZQo7LiHSxGWpYKuPq1wu
    Branch: claude/issue-5582-mapdataerror-5xx-passthrough
    Worktree: objectstack-issue-5582
    Domain: domain:cli (as labeled)
    File surface: packages/rest/src/rest-server.ts (mapDataError passthrough branch), the two pins the body names (packages/rest/src/rest.test.ts ~:2328 "does NOT pass through an explicit 5xx status", packages/rest/src/rest-4xx-message-truncation.test.ts ~:145 "5xx never enters this branch at all"), new coverage, changeset. Criterion spelling: the existing declaresServerFault predicate from @objectstack/types (per the 08-07 triage reading — read, not re-invented). ⛔ No driver-side edits: the drivers already declare truthfully (ADR-0112, #5907); the flattening is consumer-side.
    Container & model: M card, mode:subagent, model: opus
    Serial constraints cleared: in-flight #6878/PR #7396 is plugin-hono-server + qa/http-conformance — file-disjoint; queue entries pr-7304/7306/7379/7187/7201 none touch packages/rest.
    Live-producer note for the implementer: since #5907, driver-sql/driver-turso throw status=501 code=NOT_IMPLEMENTED for count_distinct/array_agg/string_agg and that reaches mapDataError's direct call sites today — the fix's acceptance case should show 501/NOT_IMPLEMENTED surviving instead of degrading to 500/INTERNAL_ERROR.


    Generated by Claude Code

  8. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    ACCEPT — PR #7402 reviewed and being driven to landing. All 26 check runs green (ESLint and TypeScript Type Check conclusions verified directly, not from the report).

    What shipped: mapDataError's explicit-status passthrough widened to 400–599 — the same door resolveErrorResponse opens — with #5437's ruled 5xx disposition: keep the declared status, keep the machine-readable code, drop the prose unconditionally. The 4xx arm (truncation, object retained, #5423) is untouched. The live #5907 producer now surfaces 501 NOT_IMPLEMENTED on the CRUD data routes instead of degrading to 500 INTERNAL_ERROR; both pins the body named were rewritten to the new contract, and a new 28-case suite covers the band, half-declarations, sanitization, and three in-process route walks.

    The dev corrected the dispatch, and the correction stands — acknowledged in the open. The dispatch said to gate the 5xx passthrough on declaresServerFault; the dev gated the status on the band and used the predicate for the code half only, with measured grounds: declaresServerFault requires a non-empty code, so predicate-gating the status would have left a no-code declared 502 still re-derived from message text — the exact #5437 shape — and both original pins are built on a no-code 502. resolveErrorResponse answers that shape with status preserved and no code invented ("the PRODUCER names the condition"), and mapDataError now matches it exactly. That is full parity, which was the ruled intent.

    Ordering decision (PM's call under the escalation bar, veto window open): the dev's open question — should a declared 5xx outrank the text classifiers below — is resolved as A, as shipped. Grounds: resolveErrorResponse's door likewise runs first, #5437 ruled against a declared status being overwritten by a keyword heuristic, and option B would re-create precisely that defect for the inputs the heuristics recognise. No live producer declares a 5xx with classifier-recognisable text (scanned), and the new §4 case pins the live half: undeclared driver conflicts still answer 409 UNIQUE_VIOLATION across all three dialects. This is recorded here rather than escalated because existing rulings determine the answer; the maintainer can veto by commenting.

    Follow-up filed as #7407 (driver docblock paragraph falsified by this PR — comment-only, finding, deliberately unqueued).


    Generated by Claude Code

  9. added 4 commits that reference this issue on Aug 17, 2026
    07383fe
    a76a3af
    d6f3f2f
    8f266f1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions