Skip to content

Console discards the server error message on 403, always showing the generic "no permission to save" string #7366

Description

@baozhoutao

Summary

When a server-side hook rejects a write with 403, the Console form discards the server's
message entirely
and substitutes a generic string. Any actionable, localized guidance the
application author wrote into the guard never reaches the user.

Non-403 errors are handled correctly — the server message is shown. This is specific to the
403 branch.

Where

@objectstack/console@17.0.0-rc.5, form submit error handler
(dist/assets/ui-components-*.js):

let n = F(e) ? i(`form.noPermissionToSave`) : ie(e) ?? i(`form.submitFailed`);
  • F is the forbidden predicate (dist/assets/framework-*.js):

    function oo(e){
      if ((e.httpStatus ?? e.status ?? e.statusCode) === 403) return true;
      const n = $(e.code, e.details?.code) ?? '';
      if (n === 'PERMISSION_DENIED' || n === 'FORBIDDEN') return true;
      …
    }
  • ie(e) is the server-message extractor — used on every other branch, but not on the 403 branch.

  • form.noPermissionToSave = You don't have permission to save this record. /
    您没有权限保存这条记录。 / このレコードを保存する権限がありません。

So the substitution is unconditional for 403: there is no "fall back to generic only when the
server said nothing" check.

Why this matters

REST returns the guard's message correctly — we verified the 403 body carries the full text.
It is discarded at the Console layer, so the application has no way to talk to the user on a
403 path
: not via message, not via an override hook, not via i18n (the key is a fixed generic
string, not a template that could interpolate the server text).

Concretely, in our app 11 hook guards return 403 with messages that tell the operator why
they were refused and what to do instead (e.g. which role owns the action, whom to ask).
The user sees only "您没有权限保存这条记录。" and has to call a developer — which is exactly
the outcome the guards were written to prevent.

This also creates an unpleasant incentive: to get their message shown, application authors are
pushed to return 400 instead of 403 for permission failures — degrading the status-code
semantics that logs, monitoring, and API consumers depend on. We are deliberately not doing
that, and are reporting this instead.

Expected

Prefer the server-provided message when present; fall back to the generic string only when the
403 response carries no usable message. Roughly:

const n = F(e) ? (ie(e) ?? i(`form.noPermissionToSave`))
               : (ie(e) ?? i(`form.submitFailed`));

If the current behavior is deliberate (e.g. to avoid leaking authorization internals through
403 bodies), then please expose an opt-in — a form-level prop, a config flag, or an i18n
template that can interpolate the server message — so applications that want to speak to
their users on 403 can do so. Silently dropping the message with no override is the part that
hurts.

Repro

  1. Object with a beforeUpdate hook that throws Object.assign(new Error('中文业务提示…'), { status: 403 });
  2. Trigger it from the Console record edit form;
  3. Network tab: the 403 response body carries the full message;
  4. UI: only the generic form.noPermissionToSave string is shown.

Found while building an app on 17.0.0-rc.5; reported per "platform issues go to the platform repo".
Related but distinct: #7307 (409 DELETE_RESTRICTED message content; this one is about the
403 message being discarded).

Activity

  1. claude commented on Aug 10, 2026

    @claude
    Contributor

    Triage: needs-user-decision — the reported behavior is a deliberate, documented decision, so this is a product call, not a plain defect.

    Evidence (objectui origin/main @ 1592b21, packages/components/src/renderers/form/form.tsx:1254-1264): the 403 branch carries a comment block explaining that a rejected write used to render the raw server text, which leaked the object machine name and record id untranslated (objectstack#3821 quoted in the code: "FORBIDDEN: insufficient privileges to update showcase_private_note pi-TgoJ4_DM55Fqz"), so permission denials deliberately render localized copy and keep the server text on the console. The components CHANGELOG records the same rationale.

    So the ask is to revise or parameterize a deliberate anti-leak ruling. Options:

    Recommendation: C. The reporter's 11 hook guards writing operator guidance is a real use case, and the incentive they name (returning 400 instead of 403 to get a message through) is a worse outcome than an opt-in. B silently re-opens the documented leak.

    Landing once ruled: objectui (packages/components form error handler) — routing/transfer deferred until the ruling.
    Dedup: no open card in either repo names the 403 message discard; #7307 (409 DELETE_RESTRICTED message content) is adjacent, different branch.

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


    Generated by Claude Code

  2. claude commented on Aug 10, 2026

    @claude
    Contributor

    Routing only: repo:objectui appended. The decision state is untouched — this card stays in the maintainer's inbox; the label exists so that once the ruling lands, a seat is wired to pick up execution (an unrouted decision card is invisible to every lane at exactly the moment it becomes actionable).

    Landing anchor (read on objectui origin/main @ 6b6721c, not from the dist bundles the card quotes): the unconditional 403 substitution is packages/components/src/renderers/form/form.tsx:1262 (? t('form.noPermissionToSave') on the forbidden branch, server-message extractor used on every other branch) — the fix lands wholly in objectui console/components code, so post-ruling execution belongs to the objectui whole-repo seat (file-at-destination, #7167).

    No grading change, no ownership taken.

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


    Generated by Claude Code

  3. claude commented on Aug 11, 2026

    @claude
    Contributor

    Maintainer ruling — 2026-08-11. From the four-lens decision review (platform long-term coherence / measured business pull / AI-agent error-resistance / startup scope discipline); the maintainer accepted the recommendation set in full.

    Ruling (paired with #7450 and the #7449 developer-half direction, same session): application-authored rejection messages are user-facing — the console's 403 branch must render the server message when the rejection originates from an application hook/validation; platform security denials keep the generic string. Concretely: the extractor (ie(e)) runs on the 403 branch too, gated by an origin discriminator rather than being skipped wholesale. State: needs-user-decision → pm:queue (lands in objectui console; repo:objectui already set).

    Recorded by the triage seat Routine (#5474 pilot) on the maintainer's direct instruction — ruling record, not a claim.


    Generated by Claude Code

  4. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Moved to objectstack-ai/objectui#5210 — closing here as moved, not as resolved. The defect is live and unfixed.

    Provenance, three parts: whose instruction — the maintainer's; verbatim — 「转」, following 「objectstack 仓库中还有很多 objectui 任务为什么没关掉」; where — said directly to the repo:objectui PM seat in session session_01RV6yuVCxymHYE16PL9vQkE, 2026-08-18, under the standing direct-dispatch authorisation.

    Why moved rather than closed: file-at-destination. Strip objectstack out of this report and it still stands whole — the fix lands entirely in the Console form's submit error handler. @baozhoutao filed it here correctly, per "platform issues go to the platform repo", before the 2026-08-10 routing ruling existed; it is being routed now, not faulted.

    @baozhoutao — your report is not being dismissed. It carried over verbatim, it keeps its pm:queue grading, and it is now in the queue of the seat that can actually land it. It ranks in that seat's top tier, because the maintainer's standing order for it is to prioritise bugs and user-visible experience, and this is squarely that: 11 hook guards in your application currently cannot say anything to your users on a 403 path. The point about the perverse incentive — that authors get pushed toward returning 400 for permission failures to make their message visible, degrading status-code semantics for logs and monitoring — carried over too, because it is the strongest argument in the report and I did not want it lost in the move.

    The 3 comments on this thread stay readable here and are worth reading before anyone acts; they were not copied.

    Anyone tracking this: follow objectstack-ai/objectui#5210.


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions