Skip to content

fix(data-objectstack): a ConnectionError nothing answered no longer carries an invented 503 - #29947

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-ji526n
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-ji526n

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #29916

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:后端连不上时(主机不可达、请求被浏览器直接拒绝),适配器的 ConnectionError 不再带一个编造的 503;服务器真正回答过的 503/504 照旧带状态码,发现接口被服务器以错误状态拒绝时带的是服务器的真实状态。
  • 为什么改:带着 503 的断网错误被列表页、记录页等读取方当成「服务器已回答」,于是把浏览器的 "Failed to fetch" 当成服务器原话展示,而不是提示用户检查网络;fix(data-objectstack): a request that got no answer no longer reads as an answered 500 #29915 修了 createErrorFromResponse 的同类问题,这是剩下的那个来源。
  • 风险与回滚:风险低。只在没传状态码时行为变化(statusCode 由 503 变为未定义,错误类与 code 不变);仓内没有任何读取方按 503 分支;回滚即还原本 PR。
  • 先看:ui/packages/data-objectstack/src/errors.ts(构造器去掉 || 503)、ui/packages/data-objectstack/src/index.ts(connect() 的 catch 与发现接口 !res.ok 分支)、ui/packages/react/src/utils/error-message.adapter-connection-unanswered-29916.test.ts(真实适配器 + 共享读取规则的边界钉)。
  • 两分钟验证:在仓根运行 pnpm --dir ui exec vitest run packages/react/src/utils/error-message.adapter-connection-unanswered-29916.test.ts packages/data-objectstack/src/connectRetriesDiscovery-12318.test.ts,预期 9 个测试全部通过,其中断网用例读作 network,已回答的 503 用例读作已回答。
  • 需要你做的:无,席位落地。

What changes

The ConnectionError constructor no longer invents a status. It stamped statusCode || 503; it now passes statusCode through, so an error built without one keeps statusCode undefined. Same shape as #29915 for createErrorFromResponse.

I moved the default in the constructor, not at the two callers. The constructor's only status input is an optional number, so a caller could not say "no answer" to it. Every caller that omits a status is a no-answer path.

Every new ConnectionError( caller, and what it carries now:

Caller Path Before After
index.ts connect() catch, discovery fetch rejected nothing answered 503 (invented) none
index.ts connect() catch, discovery answered !res.ok server answered 503 (invented, whatever the server said) the server's status (res.status)
index.ts noteServerUnanswered (post-connect error state) nothing answered 503 (invented) none
errors.ts createErrorFromResponse case 503 server answered 503 503 (unchanged)
errors.ts createErrorFromResponse case 504 server answered 504 504 (unchanged)

Why the second row matters: the connect() catch covers both a rejected fetch and a discovery the server refused. Dropping the default alone would have flipped an answered discovery failure from "answered (wrong status)" to "unanswered". So the !res.ok throw now puts status: res.status on its error, and the catch hands a numeric status to the constructor. A discovery 401 now reads as unauthorized.

Every data method opens with await this.connect(). So a list or record read against an unreachable backend rejects with the connect() error, and answeredLoadFailure / classifyLoadError read it. After this PR that read returns null and classifies as network.

Exported contract (the PM's mechanism assumption, measured): ConnectionError and ObjectStackError.statusCode?: number are exported. The TypeScript type is unchanged, since statusCode was already optional. The runtime value changes for new ConnectionError(msg) with no status: 503 becomes undefined. Docs that describe it:

  • ui/packages/data-objectstack/README.md L582 calls it "Network/connection errors (503/504)". That still describes the answered 503/504 cases, but it does not say a no-answer error carries no status.
  • content/docs/frontend/data-objectstack.mdx L497 shows "code 'CONNECTION_ERROR', statusCode 503" for a getObjectSchema failure. That path only builds a ConnectionError from an answered 503/504 (no-answer goes through createErrorFromResponse to UNKNOWN_ERROR since fix(data-objectstack): a request that got no answer no longer reads as an answered 500 #29915), so it stays true.

I left both docs as they are, because neither is in the claimed file surface. A wording follow-up for the README line is in the report.

Tests

  • New ui/packages/react/src/utils/error-message.adapter-connection-unanswered-29916.test.ts. It sits next to fix(data-objectstack): a request that got no answer no longer reads as an answered 500 #29915's boundary pin, because the adapter cannot import answeredLoadFailure (react depends on data-objectstack). It drives the real ObjectStackAdapter over a fake fetch:
    • connect() over a rejecting fetch: ConnectionError, statusCode undefined, answeredLoadFailure null, classifyLoadError network.
    • find() before connect with discovery unreachable: same.
    • after a good connect, an unanswered find() trips noteServerUnanswered: the error state event's ConnectionError has no status, null, network.
    • CONTROL: createErrorFromResponse({ status: 503 }) keeps 503 and reads as answered.
    • CONTROL: a discovery the server answered with 503 keeps 503 and reads as answered.
    • a discovery answered 401 carries 401 and classifies unauthorized.
  • Moved pin: connectRetriesDiscovery-12318.test.ts, "refused past the budget". It asserted statusCode: 503 on a discovery that was refused and never answered. It now asserts statusCode undefined.
  • errors.test.ts: the bare-constructor pin moves from 503 to undefined. A new CONTROL checks that a passed 503 / 504 is kept.

Acceptance notes

  • A test file outside the claimed surface: the new pin lives in ui/packages/react/src/utils/, not in data-objectstack. This is fix(data-objectstack): a request that got no answer no longer reads as an answered 500 #29915's precedent, for the reason above. No other file outside the claim is touched.
  • No consumer in ui/ branches on a ConnectionError's 503. I grepped statusCode === 503, status === 503 and CONNECTION_ERROR outside tests. The two 503 hits (metadata-admin/external/api.ts, preview/commitHistory.ts) read raw fetch responses, not this class.

Verification

All of it ran on head 7aaf1de9.

  • pnpm --dir ui exec vitest run packages/data-objectstack/ packages/react/src/utils/ (under the verify lock): VERDICT command-exit 0, 123 files and 1407 tests passed.

  • Consumer sweep: all 60 test files outside data-objectstack that build a real ObjectStackAdapter (app-shell, console and others, found with grep -rl "new ObjectStackAdapter|createObjectStackAdapter"). VERDICT 0, 60 files and 487 tests passed.

  • cd ui && pnpm exec turbo run build type-check --filter=@object-ui/data-objectstack --filter=@object-ui/react: VERDICT 0, 44/44 tasks. @object-ui/data-objectstack:type-check and @object-ui/react:type-check are both in the output.

  • node scripts/check-sdui-manifest.mjs: exit 0, intact, 106 components.

  • Ablation, on committed head 7aaf1de9 with scripts/ablation-replace.mjs. Vitest aliases @object-ui/data-objectstack to src, so no dist rebuild was needed.

    • I put statusCode || 503 back in the constructor. 5 tests went red: the moved 12318 pin, the errors.test.ts bare-constructor pin, and the three no-answer pins in the new file. The other 1402 stayed green, including every CONTROL. On restore, the blob was 35dfadc2 == HEAD and git diff HEAD was empty.
    • I dropped the discovery status hand-off (passed undefined). 2 tests went red: the answered-503 discovery CONTROL and the 401 pin. The other 4 stayed green. On restore, the blob was 2098bca3 == HEAD.
Derived gates: dispatch-gates --commands, 36 at 7aaf1de, all exit 0; --ran: 36 derived, 36 run, 0 NOT-MEASURED
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

pnpm --dir ui exec vitest run scripts/: 170 files passed, 2 skipped (4979 tests).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (CI runs it)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (except the 维护者速读 block the dispatch contract requires)
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzfeRhCDTUFg4Reps3f

…arries an invented 503

The ConnectionError constructor stamped `statusCode || 503`. connect()'s
catch (a rejected discovery fetch) and the post-connect error state build
it with no status, so answeredLoadFailure read the browser's
"Failed to fetch" as the server's answer. The constructor now keeps
statusCode undefined when none is passed; a discovery the server answered
with an error status carries that status.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfeRhCDTUFg4Reps3f
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd

维护者速读(席位终稿)

  • 改了什么:后端连不上时(主机不可达、请求被直接拒绝),数据适配器报的连接错误不再带一个编造的 503;服务器真正回答过的 503/504 照旧带状态码;发现接口被服务器以错误状态拒绝时,带的是服务器的真实状态(如 401)。
  • 为什么改:带 503 的断网错误被列表页、记录页当成「服务器已回答」,于是把浏览器的 "Failed to fetch" 当成服务器原话显示,而不是提示用户检查网络。fix(data-objectstack): a request that got no answer no longer reads as an answered 500 #29915 修了同类的 500,这是剩下的来源。
  • 风险与回滚:低。只有不带状态码构造的连接错误行为改变(503 → 无);仓内没有读取方按 503 分支;回滚即还原本 PR。
  • 先看:ui/packages/data-objectstack/src/errors.ts 构造器、index.ts 的 connect() catch 与发现接口 !res.ok 分支。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/react/src/utils/error-message.adapter-connection-unanswered-29916.test.ts packages/data-objectstack/src/connectRetriesDiscovery-12318.test.ts,9 个测试全过。
  • 需要你做的:无,非受管面,CI 全绿后席位落地。
  • 席位意见:ACCEPT。修法与 fix(data-objectstack): a request that got no answer no longer reads as an answered 500 #29915 同形,答复路径与无答复路径各有正控,消融两轮都打红。

席位核对 · ACCEPT · head 7aaf1de9385a3b74a854f246678de4266a371637 · 2026-11-03T16:36Z

  1. Shape: draft, base main, body opens with the attribution block then Fixes #29916 (the card closes on merge, correct: the whole card lands). One changeset (@object-ui/data-objectstack patch) carrying Clause-②: no / Metadata-protocol: none; no content/docs/releases/; 6 files, +173/−5. check-governed-merges --pr 29947: NOT governed, 178 lines ≤ 3000. No contract face (no packages/spec/src/**, no governed path, Clause-②: no), so no at-tier contract-review record is owed; this seat's own read is the review of record.
  2. Premise, measured: at origin/main 21510bba errors.ts:135 stamped statusCode || 503 and index.ts built ConnectionError with no status at ~3638 and ~3793. The diff removes the default and threads res.status through the discovery !res.ok throw, so the answered-discovery path does not flip to unanswered (the dev caught that second-order effect; read in the diff).
  3. Gates: dev's local union is pinned to 7aaf1de9: vitest 123 files / 1407 tests, consumer sweep 60 files / 487 tests, build + type-check 44/44, dispatch-gates --commands 36/36 exit 0, --ran 0 NOT-MEASURED, check-sdui-manifest exit 0. Ablation: restoring || 503 turns 5 pins red, dropping the status hand-off turns 2 red, every CONTROL stays green. Check-runs on this head were still in progress at this reading; landing waits for every check green or expected-skip on this head.
  4. Accepted deviation: the new boundary pin lives in ui/packages/react/src/utils/ (outside the claimed data-objectstack surface) because react depends on data-objectstack, not the reverse; same placement as PR fix(data-objectstack): a request that got no answer no longer reads as an answered 500 #29915. Test-only, same gate family.
  5. Out of scope: README ui/packages/data-objectstack/README.md L582 ("Network/connection errors (503/504)") — dropped, it still describes the answered 503/504 cases and misleads no reader; content/docs/frontend/data-objectstack.mdx L497 stays true (dev's reading, confirmed: that path builds from an answered 503/504 only). Skills-docs: not owed (Metadata-protocol: none). mcp_calls: one read (issue_read), no write tool.
  6. Landing: ordinary queue. When every check on this head is green or expected-skip: relay pr_ready + automerge_enable, then follow to MERGED and record the landing on data-objectstack: ConnectionError stamps 503 on a connect or request that got no answer, so a dropped connection reads as an answered server error #29916.

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.

data-objectstack: ConnectionError stamps 503 on a connect or request that got no answer, so a dropped connection reads as an answered server error

1 participant