Skip to content

fix(console): a refused edit-form record read says a localized line, never the response body - #29322

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-ovtwj0
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-ovtwj0

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

Requested by Steedos · project thread

Fixes #29186

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:控制台编辑表单(/forms/NAME?recordId=ID)读取记录被拒时,横幅不再显示「Could not open record "ID" of OBJECT for editing (403): {…}」,改为一句本地化的话:无读权限(403 / PERMISSION_DENIED)显示「您没有权限查看这条记录。」,404 显示「这条记录不存在,或者您没有权限查看它。」(行级安全把看不见的行也答成 404,两者不可区分),其他失败显示「无法打开这条记录,请重试。」。
  • 为什么改:没有读权限的成员打开编辑链接,看到的是英文 JSON,还暴露了记录 id 和对象机器名;console FormPage: a refused create shows the raw server JSON envelope to the end user ("Create failed (403): {"error":…}) #29019 已修好提交路径,加载路径还没有。
  • 风险与回滚:只动 FormPage.tsx 的加载失败分支,复用 fix(console): a refused form submit says a localized sentence, never the server's response body #29184 的 WriteRefusal / refusalText / refusalContent;十个语言包各加三个 form 键(@object-ui/i18n 的发布面因此变宽,声明行按席位修正为 yes (widening),changeset 仍为 patch)。回滚即还原此 PR。
  • 先看:ui/apps/console/src/components/FormPage.tsx 中 refusalText 的 verb === 'Load' 分支和加载器的 throw new WriteRefusal('Load', …);FormPage.loadRefusal-29186.test.tsx。
  • 两分钟验证:cd ui && pnpm exec vitest run apps/console/src/components/FormPage.loadRefusal-29186.test.tsx,预期 6 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

ui/apps/console/src/components/FormPage.tsx, the ten i18n packs, tests and a changeset:

  • The load throws the refusal the submit throws. The recordId branch of loadInternalForm no longer builds Could not open record "ID" of OBJECT for editing (STATUS): BODY. It throws new WriteRefusal('Load', status, body): the parser PR fix(console): a refused form submit says a localized sentence, never the server's response body #29184 added for writes, its verb widened to 'Load' and kept on the error as verb. No second parser. The raw body stays on .body for a debugger.
  • refusalText reads a load on its own three lines, before any write rule, and never the server's text:
    1. a 404 as form.recordNotFoundOrHidden, asked first, so a 404 whose text happens to name an access rule still reads as not-found-or-hidden (the text-sniffing isPermissionError would otherwise tell the two apart);
    2. any other denial (403, PERMISSION_DENIED, FORBIDDEN) as form.noPermissionToOpenRecord;
    3. anything else as form.openRecordFailed.
      form.noPermissionToSave is not used for a load.
  • The loader's catch sends a WriteRefusal through refusalText; every other load error keeps its message (the object-mismatch and blank-id refusals are unchanged).
  • Three new keys in all ten packs (ar, de, en, es, fr, ja, ko, pt, ru, zh): "You don't have access to this record.", "This record doesn't exist, or you don't have access to it.", "Could not open this record. Please try again."

Measurement (showcase, before and after)

examples/app-showcase on objectstack dev --seed-admin --fresh, Console source on Vite, a member created by the admin (default member permission set). The before leg served origin/main's FormPage.tsx from the same Vite; restored after, blob equal to HEAD.

Case Server answer Banner before Banner after
/forms/showcase_contact.form?recordId=ID, member has no read right on contacts 403 {"error":"You do not have permission…","code":"PERMISSION_DENIED","object":"showcase_contact"} Could not open record "ID" of showcase_contact for editing (403): {"error":…,"code":"PERMISSION_DENIED","object":"showcase_contact"} (at 375 px the JSON runs off the banner) You don't have access to this record.
/forms/showcase_inquiry.contact?recordId=ID, the admin's inquiry, hidden from the member by the private OWD (the admin reads it 200) 404 {"error":"Record ID not found in showcase_inquiry","code":"RECORD_NOT_FOUND","object":"showcase_inquiry"} Could not open record "ID" of showcase_inquiry for editing (404): {"error":…,"code":"RECORD_NOT_FOUND",…} This record doesn't exist, or you don't have access to it.

A missing id answers the same 404 RECORD_NOT_FOUND shape as the hidden row (measured on showcase_task), so the one line covers both. Screenshots, desktop and 375 px, before and after, are in the project files under issue-29186/.

Tests

  • New FormPage.loadRefusal-29186.test.tsx (6 cases; fixtures are the measured bodies): a 403 PERMISSION_DENIED read shows the denial line, and the page text has no {, no status, no record id and no object name, with no form or Submit left behind and no write issued; the same in zh; a 404 RECORD_NOT_FOUND shows the not-found-or-hidden line; a 404 whose text says "Access denied by row-level security policy" still shows that line; a 500 shows the generic line and not the driver text; a 502 markup page shows the generic line.
  • FormPage.recordId.test.tsx: its three load-refusal pins asserted the old raw text (Could not open record "task-404".*404) and now assert the localized lines; what they guard (an error state, no create form, no write) is unchanged.
  • FormPage.refusalText-29019.test.tsx (the write refusals) passes unchanged.
  • Ablation (scripts/ablation-replace.mjs, anchor else if (e instanceof WriteRefusal) setError(refusalText(e, null, t)); x1 -> x0, replaced by a banner of the status and the raw body): the new suite went 6/6 red; restored, blob equals HEAD and git diff HEAD is empty.

Acceptance notes

  • The load banner keeps no "Go back" control and is the same panel as before; only its text changed.
  • Other load errors in loadInternalForm (form metadata Could not load form metadata (STATUS): view/NAME, the object-mismatch refusal) still draw their developer-facing message. They are outside this card's ruling (the record read); noted for the seat, not filed.

Verification

Local subset. Head 8b71e4ae (branch merged with origin/main twice, both clean; the second merge brought an unrelated key into the same packs, so the i18n suite and the FormPage suites were re-run there).

Tests
  • cd ui && pnpm exec vitest run --maxWorkers=2 apps/console/src/components/FormPage apps/console/src/components/InternalFormRoute apps/console/src/components/InAppFormRoute packages/i18n/ on f4273fb0: Test Files 139 passed (139), Tests 1839 passed, 13 skipped.
  • On 8b71e4ae: packages/i18n/ plus the three FormPage load/write refusal suites, Test Files 98 passed (98), Tests 1469 passed, 13 skipped.
  • pnpm exec turbo run type-check --filter=@object-ui/console --filter=@object-ui/i18n: 72 tasks successful (console's type-check covers src, test files included).
  • Ablation: scripts/ablation-replace.mjs on FormPage.tsx, anchor x1 -> x0, the new suite 6 failed (6); restored, blob == HEAD, git diff HEAD empty.
Derived gates (`dispatch-gates --commands`, 34 families) on `7c4f91c8`, reconciled with `--ran`: 34 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 --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:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: 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: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/ :: exit 0
pnpm check:type-check-debt :: exit 0

Re-run on the final head 8b71e4ae after the second merge (changeset, closing-keyword, manifest and ratchet families):

node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:published-files :: exit 0
pnpm check:type-check-debt :: exit 0

Every other family is CI's: Lint UI, Type Check UI, Test UI and the rest of the required set on this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QsR2jSXNWqmd9k2JHw7drz


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 8fb9e1225bab7fe9351b46cefc4b08d82cfd9c28
Local-runs: none

Face: declaration. The PR body and the changeset declare Clause-②: yes (widening) (claim amended 6576516998). No published schema and no governed path are touched: check-governed-merges --pr 29322 hit 0 of 14 paths, and nothing is under packages/spec/src/**. So the review owed is the lane seat's TIER_DEFAULT record.

① Derived judgments

② Semver level

The changeset entries are patch, with Metadata-protocol: none. AGENTS.md Post-Task Checklist step 3: a non-protocol change, widening or breaking, ships patch. Consistent.

③ Boundary flags

  • Deviation, accepted: the code was written before the live measurement. The before leg served main's FormPage.tsx, so the reading holds.
  • The PR author is objectstack-fleet[bot], so there is no off-relay writer to name.

Implemented-by: claude/project-thread-ovtwj0
Reviewed-by: session_01QsR2jR7ZFkpqmm4U8S1f9y

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

席位核对 · ACCEPT · 2026-11-02T12:19Z

Head-sha: 8fb9e1225bab7fe9351b46cefc4b08d82cfd9c28

  • Shape: draft → main, Fixes #29186 in the body only. 14 files, +255 / −11 (266 lines ≤ 3000). No governed path: check-governed-merges --pr 29322 hit 0 of 14 paths. Author objectstack-fleet[bot]. Clause-②: yes (widening) (claim amended 6576516998; the body was patched by the seat) and Metadata-protocol: none are line-initial in the body and the changeset.
  • Contract review: the seat's TIER_DEFAULT record 6576954850 on this head, PASS. The widening is three new form.* keys in each of @object-ui/i18n's ten packs.
  • File surface: FormPage.tsx, the ten locale packs, the new test file, the recordId suite's three load pins, and the changeset, exactly the claim.
  • Read in the diff:
  • Measured (in the body): on the stock showcase, a member's read answered 403 PERMISSION_DENIED, and a private-OWD row answered 404 RECORD_NOT_FOUND. Before the fix, both banners drew the status and the JSON body, which ran off at 375 px. After, they show the localized line. The screenshots are in the project folder.
  • Tests: the new suite has 6 cases, and its ablation (raw body back on the load path) turns 6 of 6 red. The neighbouring 139 files pass, and the console and i18n type-checks pass. 34 of 34 derived gates exited 0, and the body gates exited 0 with the new body.
  • Gates (check-runs on this head): 45 success, 13 skipped, 0 failed or pending, including all 16 Test UI shards. The one cancelled run was the PR-body guard superseded by the body patch; its rerun succeeded. All ten required contexts succeeded, or were skipped by their path filter (Build Core).
  • Serial: domain:ui#5's console form link: a user without create right is offered a fillable create form on /forms/NAME, and learns of the refusal only on Submit #29187 edits the same file and starts after this lands.
  • Landing: ordinary queue landing, pr_ready then automerge_enable. Fixes #29186 closes the card on merge.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 12:21
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 4e0ff3e Nov 2, 2026
59 of 60 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ovtwj0 branch November 2, 2026 13:18
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

Landed · 2026-11-02T13:41Z · PR #29322 merged through the merge queue as 4e0ff3e6. Record: ACCEPT 6576966891 at 8fb9e122 (Clause-②: yes (widening), seat TIER_DEFAULT record 6576954850 PASS).

probe before (4e0ff3e6~1) now (origin/main)
throw new WriteRefusal('Load', recRes.status, body); in ui/apps/console/src/components/FormPage.tsx (a refused record read is a refusal, not a raw body) 0 1
recordNotFoundOrHidden in ui/packages/i18n/src/locales/zh.ts (the localized 404 line) 0 1

The card closed through Fixes. pm:dispatched comes off with this note. domain:ui#5's #29187 (same file, serial behind this PR) can start now.


Generated by Claude Code

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 tooling

Projects

None yet

1 participant