Observation
Found while implementing objectui#7907 (the managed-snapshot refresh swallowing a failed
read), while re-measuring the response shapes fetchFullPackage can meet. Deliberately
not fixed there: that card's file surface is the tail of onManageChanged, and this
sits in fetchFullPackage itself — PR #7906's code, shared by all four of its callers.
fetchFullPackage in
packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx reads a failure
envelope like this:
const failure = await res.json().catch(() => null);
const message = typeof failure?.error?.message === 'string' ? failure.error.message : '';
const code = typeof failure?.error?.code === 'string' ? failure.error.code : '';
throw new Error(message ? (code ? `${message} (${code})` : message) : `HTTP ${res.status}`);
It reads error.message and error.code. It does not read error.userMessage.
Why that field matters here
GET /api/v1/packages is served by the framework's direct-mount registrar, whose
sendThrownError forwards a producer-marked userMessage onto the wire (framework
#12502, pinned by packages/rest/src/package-door-user-message.test.ts). Per ADR-0112
that field is text the producer marked at throw time as addressed to the end user,
and a consumer that sees it is meant to render it verbatim.
The two facts compose badly in exactly the band where it counts. In the 5xx band the
door withholds the producer's prose and substitutes the generic
Internal server error into error.message — but the withholding is scoped to
message, and userMessage is deliberately not withheld (that door's own note says
so: a marked text is the producer's deliberate statement to the caller at any status).
So on a 500/503 that carries a marked text, the Studio shows the author the generic
sentence and silently drops the specific one written for them.
Nothing invalid is displayed, which is what makes it quiet: the toast is correct, just
generic, and the marked sentence has nowhere to appear.
Scope
The read is shared, so this is one fix at one site with four beneficiaries: the switcher
list lookup behind "Package info and settings", the writability courtesy gate, the
namespace lookup, and the managed-snapshot refresh.
Whether the sibling apiJson helper in
packages/app-shell/src/views/metadata-admin/PackagesPage.tsx should read the same field
is part of the same question — it has the identical payload?.error?.message ladder and
also serves lifecycle actions.
Worth deciding
- Prefer
error.userMessage over error.message when present, everywhere the envelope
is read on this surface.
- Same, but only in the 5xx band, where the generic substitution is what makes the loss
visible.
- Leave it and record why — e.g. if the marked channel is judged not reachable on the
package read path in practice.
Not measured in this card: whether any thrower on the package list path specifically
sets a marked text today. The door emits the field and the framework pins it; whether
PackageService.list() or the metadata-protocol overlay read ever marks one was not
established. If the answer is "not today" this is a latent gap rather than a live one,
which is a fair reason to prefer option 3 — but it should be the recorded answer, not an
assumption.
Re-check
grep -n "failure?.error?.message" packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx
grep -rn "userMessage" packages/app-shell/src/views/
Related, not duplicates: objectui#7368, objectui#7821 (PR #7879), objectui#7881
(PR #7906), objectui#7907 — the same family of "a read that could not happen is reported
as a read that found nothing", where this one is "a refusal that was explained is
reported with the generic explanation".
Generated by Claude Code
Observation
Found while implementing objectui#7907 (the managed-snapshot refresh swallowing a failed
read), while re-measuring the response shapes
fetchFullPackagecan meet. Deliberatelynot fixed there: that card's file surface is the tail of
onManageChanged, and thissits in
fetchFullPackageitself — PR #7906's code, shared by all four of its callers.fetchFullPackageinpackages/app-shell/src/views/studio-design/StudioDesignSurface.tsxreads a failureenvelope like this:
It reads
error.messageanderror.code. It does not readerror.userMessage.Why that field matters here
GET /api/v1/packagesis served by the framework's direct-mount registrar, whosesendThrownErrorforwards a producer-markeduserMessageonto the wire (framework#12502, pinned by
packages/rest/src/package-door-user-message.test.ts). Per ADR-0112that field is text the producer marked at throw time as addressed to the end user,
and a consumer that sees it is meant to render it verbatim.
The two facts compose badly in exactly the band where it counts. In the 5xx band the
door withholds the producer's prose and substitutes the generic
Internal server errorintoerror.message— but the withholding is scoped tomessage, anduserMessageis deliberately not withheld (that door's own note saysso: a marked text is the producer's deliberate statement to the caller at any status).
So on a 500/503 that carries a marked text, the Studio shows the author the generic
sentence and silently drops the specific one written for them.
Nothing invalid is displayed, which is what makes it quiet: the toast is correct, just
generic, and the marked sentence has nowhere to appear.
Scope
The read is shared, so this is one fix at one site with four beneficiaries: the switcher
list lookup behind "Package info and settings", the writability courtesy gate, the
namespace lookup, and the managed-snapshot refresh.
Whether the sibling
apiJsonhelper inpackages/app-shell/src/views/metadata-admin/PackagesPage.tsxshould read the same fieldis part of the same question — it has the identical
payload?.error?.messageladder andalso serves lifecycle actions.
Worth deciding
error.userMessageovererror.messagewhen present, everywhere the envelopeis read on this surface.
visible.
package read path in practice.
Not measured in this card: whether any thrower on the package list path specifically
sets a marked text today. The door emits the field and the framework pins it; whether
PackageService.list()or the metadata-protocol overlay read ever marks one was notestablished. If the answer is "not today" this is a latent gap rather than a live one,
which is a fair reason to prefer option 3 — but it should be the recorded answer, not an
assumption.
Re-check
Related, not duplicates: objectui#7368, objectui#7821 (PR #7879), objectui#7881
(PR #7906), objectui#7907 — the same family of "a read that could not happen is reported
as a read that found nothing", where this one is "a refusal that was explained is
reported with the generic explanation".
Generated by Claude Code