Observation
Found while implementing objectui#7938 (fetchFullPackage dropping the producer-marked
error.userMessage), while establishing which of that endpoint's readers actually parse
the envelope. Deliberately not fixed there: that card's file surface is fetchFullPackage
inside StudioDesignSurface.tsx, and this sits in a different module.
fetchPackages in packages/app-shell/src/views/studio-design/packages-io.ts reads a
failure like this:
export async function fetchPackages(): Promise<PkgEntry[]> {
const res = await fetch('/api/v1/packages', { /* … */ });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return parsePackages(await res.json());
}
It never opens the body. Not error.message, not error.code, not error.userMessage —
the response is discarded and the status becomes the whole report.
Why that matters, measured
GET /api/v1/packages has FOUR readers on the Studio design surface. The
openManage docblock in StudioDesignSurface.tsx names them, and they split two ways:
| reader |
goes through |
reads the envelope? |
switcher list (mount effect + onManageChanged head) |
fetchPackages |
no |
| writability courtesy gate (ADR-0124 D1) |
fetchPackages |
no |
| namespace lookup (framework#2694) |
fetchPackages |
no |
fetchFullPackage (manage sheet + snapshot refresh) |
its own reader |
yes |
Only the fourth parses the body. So three of the four surface a 403 FORBIDDEN whose
message names the very capability to grant — Reading packages requires the studio.accessorsetup.access capability. — as the four characters 403.
All three report through formatMetadataError on the shared studio-package-list
sonner id (objectui#7368's posture), so the plumbing to display words is already there
and already wired. What is missing is words to put in it: formatMetadataError returns
err.message, and the message it is handed is HTTP 403.
This is a strictly larger loss than objectui#7938's. That card was about ONE optional
field being dropped from a body that was otherwise read; this is the body never being
read at all — message, code and userMessage alike.
Why it is a separate card and not a rider
Three reasons, all of them boundary rather than size:
- Different file. objectui#7938's declared file surface is
fetchFullPackage plus one
pin test in the same package; packages-io.ts is neither.
- Different blast radius.
packages-io.ts is shared with the builder landing page
(parsePackages's module doc says so), so its readers are not all on the Studio
design surface and its callers were not all measured by that card.
- Different question underneath.
fetchFullPackage and fetchPackages now hold two
independent implementations of "read the ADR-0112 failure envelope", and the real
decision is probably whether that ladder should be ONE shared helper rather than
whether to copy it a second time — see below.
Worth deciding
- Give
fetchPackages the same ladder fetchFullPackage now has (marked text, then
diagnostic, then code, falling back to the status). Smallest change; leaves two copies
of one rule free to drift, which is the shape that produced this card.
- Extract the envelope read into one exported helper and have both call it. Fixes the
drift as well as the loss; touches both modules and wants its own pin.
- Leave
fetchPackages bare and record why — e.g. if the three degradation-only readers
are judged to want a short status rather than a server sentence in their toast. That
is a defensible answer, but it should be the recorded one rather than the default.
Not measured in this card: whether the sibling apiJson helper in
packages/app-shell/src/views/metadata-admin/PackagesPage.tsx (flagged in objectui#7938's
own body as part of the same question) should be folded into option 2. It has the
identical payload?.error?.message ladder and also serves lifecycle actions.
Re-check
sed -n '/export async function fetchPackages/,/^}/p' packages/app-shell/src/views/studio-design/packages-io.ts
grep -n "fetchPackages()" packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx
grep -rn "userMessage" packages/app-shell/src/views/
Related, not duplicates: objectui#7938 (the fourth reader, fixed), objectui#7368 (the
report channel these three already share), objectui#7821, objectui#7881, objectui#7907 —
the same family of "a read that could not happen is reported as something else".
Observation
Found while implementing objectui#7938 (
fetchFullPackagedropping the producer-markederror.userMessage), while establishing which of that endpoint's readers actually parsethe envelope. Deliberately not fixed there: that card's file surface is
fetchFullPackageinside
StudioDesignSurface.tsx, and this sits in a different module.fetchPackagesinpackages/app-shell/src/views/studio-design/packages-io.tsreads afailure like this:
It never opens the body. Not
error.message, noterror.code, noterror.userMessage—the response is discarded and the status becomes the whole report.
Why that matters, measured
GET /api/v1/packageshas FOUR readers on the Studio design surface. TheopenManagedocblock inStudioDesignSurface.tsxnames them, and they split two ways:onManageChangedhead)fetchPackagesfetchPackagesfetchPackagesfetchFullPackage(manage sheet + snapshot refresh)Only the fourth parses the body. So three of the four surface a
403 FORBIDDENwhosemessage names the very capability to grant —
Reading packages requires thestudio.accessorsetup.accesscapability.— as the four characters403.All three report through
formatMetadataErroron the sharedstudio-package-listsonner id (objectui#7368's posture), so the plumbing to display words is already there
and already wired. What is missing is words to put in it:
formatMetadataErrorreturnserr.message, and the message it is handed isHTTP 403.This is a strictly larger loss than objectui#7938's. That card was about ONE optional
field being dropped from a body that was otherwise read; this is the body never being
read at all —
message,codeanduserMessagealike.Why it is a separate card and not a rider
Three reasons, all of them boundary rather than size:
fetchFullPackageplus onepin test in the same package;
packages-io.tsis neither.packages-io.tsis shared with the builder landing page(
parsePackages's module doc says so), so its readers are not all on the Studiodesign surface and its callers were not all measured by that card.
fetchFullPackageandfetchPackagesnow hold twoindependent implementations of "read the ADR-0112 failure envelope", and the real
decision is probably whether that ladder should be ONE shared helper rather than
whether to copy it a second time — see below.
Worth deciding
fetchPackagesthe same ladderfetchFullPackagenow has (marked text, thendiagnostic, then code, falling back to the status). Smallest change; leaves two copies
of one rule free to drift, which is the shape that produced this card.
drift as well as the loss; touches both modules and wants its own pin.
fetchPackagesbare and record why — e.g. if the three degradation-only readersare judged to want a short status rather than a server sentence in their toast. That
is a defensible answer, but it should be the recorded one rather than the default.
Not measured in this card: whether the sibling
apiJsonhelper inpackages/app-shell/src/views/metadata-admin/PackagesPage.tsx(flagged in objectui#7938'sown body as part of the same question) should be folded into option 2. It has the
identical
payload?.error?.messageladder and also serves lifecycle actions.Re-check
Related, not duplicates: objectui#7938 (the fourth reader, fixed), objectui#7368 (the
report channel these three already share), objectui#7821, objectui#7881, objectui#7907 —
the same family of "a read that could not happen is reported as something else".