Skip to content

console FormPage: a refused create shows the raw server JSON envelope to the end user ("Create failed (403): {"error":…}) #29019

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbX9avDwM2goWd7zimE

Filing gate: ① a user-visible defect, found beside the clauses (studio-authoring.no-entry-property-matrix), reach: the console named form page, measured once with a deterministic cause in source.
Business reason: an end user without create rights who opens a form link (/_console/forms/NAME) fills it in and presses Submit, and is shown the server's raw JSON in English: Create failed (403): {"error":"You do not have permission…","code":"PERMISSION_DENIED","object":"…"}. The report page tells the same user "You don't have access"; the form should give a readable, localized refusal (and ideally not offer a create form the caller cannot submit).
Reader who acts: the triage seat, then a domain:ui implementer in ui/apps/console/src/components/FormPage.tsx. QA suggestion only: area:records, p2.
Duplicate search: "Create failed" → 271 hits, the near ones closed and different (#24914 raw node text in a screen flow, #25938 FLOW_FAILED rendering); FormPage 403 raw JSON → 9 hits, all QA run records or checklist revisions.
Batch-of: release-18.3.0-w4-c3b1 — 16 cards from QA run #29002 (subject 7fa9873f; the implementer re-checks the landing site on current main).

Reproduction

  1. A member m1 with read but no create on object qar8_fsb, which has a default form view.
  2. As m1, open /_console/forms/qar8_fsb.form. The form renders fully, with no "no access" state.
  3. Fill Name and press Submit → POST /api/v1/data/qar8_fsb → 403.

Expected: a localized refusal sentence, without the JSON envelope. Actual: the banner and the toast read verbatim Create failed (403): {"error":"You do not have permission to perform this action. Contact your administrator if you need access.","code":"PERMISSION_DENIED","object":"qar8_fsb"}.

Mechanism (suspected)

FormPage.tsx about L1968–1969 builds the message as a template of the verb, the status and the raw body; refusedWriteError (about L1990) parses code and details but keeps that raw message.

QA-source: #29002 · studio-authoring.no-entry-property-matrix · observed beside the clauses

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: domain:ui · p2 · pm:queue · 2026-11-02T07:23Z

    ⛔ Not a claim.

    • Read fresh. ui/apps/console/src/components/FormPage.tsx builds the refusal banner from the raw response body, and refusedWriteError keeps that raw message, so domain:ui. An end user reading a raw English JSON envelope is a user-visible defect that needs logic beyond a string swap, p2. Its QA-source item (no-entry-property-matrix) is not this capability, so Path is none.

    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Claim: PM loop round 11 · 2026-11-02T08:02Z
    Session: session_01QsR2jRRUHCK9kYPaCjRKL2 (card thread, os-dev); dispatched by seat session session_01QsR2jR7ZFkpqmm4U8S1f9y
    Account: zhuangjianguo
    Branch: claude/project-thread-aldhur (platform-assigned to the card thread; it can push only this branch)
    Worktree: objectstack-issue-29019
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: ui/apps/console/src/components/FormPage.tsx (the write-refusal message), the i18n packs, their tests, and a changeset (stop on breach; explain in the report)
    Container & model: S, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: default
    Clause-②: no
    Metadata-protocol: none
    Responsibility: a member without create rights who submits a form link reads the server's raw JSON envelope in English | the console already parses the refusal code and has an access-denied tone for the same reader | every end user who opens a shared form link
    Thread-read: 6571498378
    Premise: holds (at origin/main, FormPage.tsx:1968-1969 builds the message as the verb, the status and the raw body)
    Serial constraints cleared: #29072 and #29021 build beside it on other files


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · zhuangjianguo · session_01QsR2jRRUHCK9kYPaCjRKL2
    os-dev-report

    {
      "issue": 29019,
      "status": "done",
      "branch": "claude/project-thread-aldhur",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29184",
      "head": "c83558d5 (bc782a0d merged with origin/main)",
      "session": "session_01QsR2jRRUHCK9kYPaCjRKL2",
      "premise_still_valid": true,
      "summary": "FormPage.tsx: refusedWriteError(message-with-raw-body) is replaced by a page-local WriteRefusal error carrying status, code, details and userMessage, whose message is the server's sentence and never the body. The banner and the toast now say one sentence, read in the record dialog's order: marked userMessage or delegation scope text, then form.noPermissionToSave for any denial, then the unmarked per-rule lines, then the server's 4xx sentence, else form.submitFailed (5xx, or no sentence such as a proxy markup page). Field markers unchanged. No i18n key added: both lines already exist in all ten packs, so the claim's Clause-2 no holds by construction.",
      "files_changed": [
        ".changeset/29019-form-refusal-text.md",
        "ui/apps/console/src/components/FormPage.tsx",
        "ui/apps/console/src/components/FormPage.refusalText-29019.test.tsx (new)",
        "ui/apps/console/src/components/FormPage.refusalMarks-27417.test.tsx",
        "ui/apps/console/src/components/FormPage.nativeInvalid-27496.test.tsx"
      ],
      "line_budget": "399 insertions, 38 deletions over 5 files (dispatch-gates: 434 changed lines vs the 3000 threshold: under)",
      "tests": "New FormPage.refusalText-29019 (7 cases: 403 PERMISSION_DENIED internal en, same in zh, wrapped 403 public, marked userMessage, 400 with fields still marks the field, 500 generic, 502 markup generic; each asserts banner == toast, no brace, no status code). On merged head c83558d5: vitest FormPage* + InternalFormRoute + InAppFormRoute = Test Files 43 passed (43), Tests 390 passed (390), lock VERDICT command-exit 0. pnpm --filter @object-ui/console type-check exit 0 (tsconfig includes src, tests compiled). Ablation via scripts/ablation-replace.mjs: anchor '? refusalText(err, unmarked, t)' x1->x0 replaced by the old 'failed (STATUS): BODY' text; new suite 7 failed (7); restore blob == HEAD f5afeaa3, git diff HEAD empty. Two older pins that asserted 'Submit failed (400)' repointed to the server sentence.",
      "gates": "dispatch-gates --commands (33 families) all exit 0; --ran reconciliation: 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN. PR-body gates with the final body as GITHUB_EVENT_PATH: check-changeset-no-major exit 0, check-adr-0087-registration exit 0, check-closing-keyword-parity exit 0, check-partof-closing-keyword exit 0. CI families (Lint UI, Type Check UI, Test UI, Build and E2E UI, the rest of the required set): in_progress on the PR, not re-run locally.",
      "gates_ran": [
        "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: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: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"
      ],
      "dogfood_battery": "n/a, no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "3, all reads: issue_read get (1), issue_read get_comments (2). No MCP write.",
      "api_writes": "2 relay writes: pr_create (POST /repos/objectstack-ai/objectstack/pulls, run 41416454415, body read back 8918/8918 bytes identical, no assignees, no labels) and this os-dev-report comment (post-stamped). git push is not a REST write.",
      "deviations": [
        "None from the brief. The harness's session-context asks to assign and request review from the user on a new PR through GitHub MCP; the brief's rule 3 and 10 forbid it, so neither was done.",
        "No new i18n strings: the brief's 'all ten packs' rule is met by reusing form.noPermissionToSave and form.submitFailed, present in all ten."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: none (measured reading the brief asked for) . Up-front refusal not done: the in-app route /apps/APP/forms/NAME renders under ConsolePermissionsScope (App.tsx pathless layout around /home and /apps/APP/*) and already holds /me/permissions; the app-less /forms/NAME route of the reproduction sits under ProtectedRoute alone, so knowing the create right there needs a new request; /f/SLUG is anonymous. Doing it on one route only would split the same form's behaviour by URL. carrier: seat (decide whether to move /forms/NAME under the scope or read permissions there). dedupe words: FormPage create permission upfront, forms route ConsolePermissionsScope, read-only form no create right",
        "class: none (read-only inference, not reproduced) . An edit form whose record read is refused still draws 'Could not open record ID of OBJECT for editing (STATUS): BODY' on its load banner (FormPage.tsx loader, the recordId branch; drawn by the error-and-not-loaded branch via refusalContent). Load path, outside this card's ruling. carrier: seat. dedupe words: Could not open record for editing raw body, FormPage recordId load error JSON"
      ]
    }
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Seat review · 2026-11-02T09:13Z · PR #29184 at c83558d5: ACCEPT (on the PR; Clause-②: no, no contract review owed). It goes to the merge queue now. Follow-ups: #29186 and #29187.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Landed · 2026-11-02T10:39Z · PR #29184 merged through the merge queue as 817cfa47. Record: ACCEPT 6573369575 at c83558d5 (Clause-②: no, no contract review owed).

    probe before (817cfa47~1) now (origin/main)
    class WriteRefusal in ui/apps/console/src/components/FormPage.tsx 0 1
    Submit failed (${res.status}): ${body in the same file (the raw server body in the message) 1 0

    The card closed through Fixes. pm:dispatched comes off with this note. Its follow-ups on the same file are now orderable: #29186 (p2, the edit form's load banner draws the raw body) and #29187 (p3, the up-front create refusal), both domain:ui#4.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:uipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions