Skip to content

fix(app-shell): tell the operator a 503 means the commit store is unreachable, not commits HTTP 503 - #4146

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3529-commit-fetch-error-text
Aug 10, 2026
Merged

fix(app-shell): tell the operator a 503 means the commit store is unreachable, not commits HTTP 503#4146
yinlianghui merged 1 commit into
mainfrom
claude/issue-3529-commit-fetch-error-text

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3529

The build-history panel is the rollback surface — an operator reads it mid-incident. It answered every failed commit-store call with a bare status code (commits HTTP 503), so an outage and a missing package looked identical while their dispositions differ.

What changed

  • packages/app-shell/src/preview/commitHistory.tsfetchCommits and the same-shaped revertCommit now throw a CommitStoreError carrying status, the ADR-0112 code, and a retryable flag. Classification keys on the HTTP status first and treats the envelope code as a second signal.
  • packages/app-shell/src/preview/CommitTimeline.tsx — the panel renders a sentence rather than a number, and the revert toast gets its own sentence.
  • Two new keys across all ten locale packs (258 个 t() 调用点引用的 key 在任何语言包里都不存在(#3530 守卫首跑实测),其中 8 处直接把 raw key 渲染给用户 #3546 discipline): preview.history.loadFailedUnavailable, preview.history.revertUnavailable. Neither takes interpolation arguments.

retryable is deliberately not "any 5xx". Only 503 / SERVICE_UNAVAILABLE means "this did not happen"; a 500 is the server saying it broke while doing the work, which a retry does not answer. 404, 500 and 503 stay tellable apart.

The revert half says something different on purpose. A write that could not reach the store may still have landed — a proxy can shed a 503 after forwarding — and re-issuing appends a second revert commit to an append-only log. So its copy asks the operator to re-read the timeline before retrying, instead of "try again".

Two premise details did not survive checking, and they are why the copy is authored client-side

The card's core premise holds: the retryable meaning was being flattened. Two supporting details in the body are wrong at the browser boundary, and both strengthen the fix rather than weaken it.

1. The code arrives at error.code, not details.code.HttpDispatcher.errorFromThrown parks the thrown error's .code in details (objectstack packages/runtime/src/http-dispatcher.ts:718), and buildApiError / splitSemanticCode then lift it into error.code and drop details entirely when it held nothing else (packages/runtime/src/error-envelope.ts:100-125, objectstack#3842). The source says so itself at http-dispatcher.ts:704. A consumer reading details.code would be running a check that can only ever pass vacuously.

2. The envelope message for this class is withheld. The card expects the 503 prose to say "unknown". declaresServerFault (objectstack packages/types/src/error-leak.ts:113) is true for exactly this error — status at or above 500 with a string code — so dispatcher-plugin.ts:521 replaces the sentence with the generic INTERNAL_ERROR_MESSAGE ("Internal server error") while error.code survives. Rendering the envelope message would have shown the operator a generic string and lost the status: strictly worse than the bare code it replaced. Hence the retryable copy is authored on the client side, and the envelope message is preferred only for the statuses where it is not withheld (4xx) — which is what keeps a 404 reading differently from a 500.

No alias chain was added beside error.code. A tolerant read of a shape this endpoint does not send is the consumer-side leniency Prime Directive #12 removes.

Reverse verification — predicted, and the prediction has two halves

Prediction recorded before running: the classification and presentation cases go red; the fail-loud cases stay green, because origin/main already failed loud (the issue body says so, and the triage comment confirmed it) — those pins are regression guards, not evidence for this change.

Both halves confirmed. Restoring both source files to origin/main and keeping the new tests: 27 of 37 red, 10 green. The 10 that held green are exactly the five fail-loud cases, the two success paths, the two pre-existing revertCommit behaviours, and the "three different messages" case — that last one passes on both sides, since commits HTTP 404/500/503 are also three distinct strings.

One honest caveat: the three fail-loud cases in CommitTimeline.test.tsxdid go red on the revert, but as an artifact — they anchor on the new data-testid="commit-history-error", which does not exist on origin/main. They are genuine guards going forward; the load-bearing fail-loud evidence is the commitHistory.test.ts block that stayed green across the revert.

Verification

  • pnpm vitest run packages/app-shell/src/preview/ — 6 files, 58 tests passed (37 new).
  • pnpm --filter @object-ui/app-shell --filter @object-ui/i18n type-check — Done, after building the closure with pnpm --filter '@object-ui/app-shell^...' build (the first run was a stale-dist false red).
  • pnpm --filter @object-ui/app-shell --filter @object-ui/i18n lint0 errors. The one warning on CommitTimeline.tsx is the pre-existing useEffect at the old line 73, untouched here.
  • node scripts/check-i18n-call-site-keys.mjs — green: every inline defaultValue matches its en value and every call site passes exactly the arguments that value has holes for.
  • node scripts/check-i18n-en-drift.mjs — green: "2 key(s) added, 0 en value(s) changed".
  • packages/i18n/src/__tests__/all-locales-key-parity.test.ts — 20 passed.

Note for the PM

The claim flagged #4118 as in flight over app-shell i18n. This PR's new keys land in packages/i18n/src/locales/*.ts (the i18next packs), not app-shell's module-local metadata-admin/i18n.ts table, so the surfaces differ; whichever lands second rebases only if #4118 also edits the ten packs.


Generated by Claude Code

…eachable (#3529)
`commitHistory.ts` flattened every non-OK response to a bare status code
(`commits HTTP {status}` / `HTTP {status}`). Nothing was swallowed and no
fictional "no history" was ever rendered — those fail-loud properties held and
still hold — but the meaning the backend already sends was lost on the one
screen where it matters most: the rollback surface, read mid-incident.
Failures now throw a `CommitStoreError` carrying `status`, the ADR-0112 `code`
and a `retryable` flag, and `CommitTimeline` renders a sentence instead of a
number. 404, 500 and 503 stay tellable apart. The revert half gets a different
sentence on purpose: a write that could not reach the store may still have
landed, and re-issuing appends a second revert commit to an append-only log.
Two details of the report were checked against the producer and came back
different, which is why the copy is authored client-side: the semantic code
arrives at `error.code`, not `details.code` (objectstack#3842 lifts it and drops
`details`), and the envelope `message` for this class is withheld as the generic
`Internal server error` (`declaresServerFault`, objectstack#5811). Classification
keys on status first, so a proxy-shed 503 with an HTML body still reads retryable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 10, 2026 1:55pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.3 KB350 KB
Entry fileindex-CFtgndeQ.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)485.06KB107.21KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)140.66KB36.25KB
fields (index.js)229.40KB56.93KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.87KB10.80KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.52KB17.49KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.52KB30.68KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)237.80KB59.48KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)162.81KB39.67KB
plugin-grid (index.js)188.04KB49.91KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)110.04KB26.67KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.95KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 10, 2026 14:04
@yinlianghui
yinlianghui added this pull request to the merge queueAug 10, 2026
Merged via the queue into main with commit f7c6430Aug 10, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3529-commit-fetch-error-text branch August 10, 2026 14:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

观察:commit 时间线拉取失败只显示 commits HTTP 503,丢掉了「commit store 不可达、可重试」这层意思

2 participants

@yinlianghui@claude