Skip to content

Commit f5cc78b

Browse files
os-litantclaude
andauthored
fix(rest): the generic declared-status passthrough names its object on both error doors (#15452)
* fix(rest): the generic declared-status passthrough names its object on both doors (#14725) `resolveErrorResponse`'s 4xx arm now ends on the same `...(object ? { object } : {})` limb `classifyDataError`'s generic declared-status passthrough has always carried, so the two copies of one passthrough no longer differ by a key. Measured on `main` @ `a12b15e394`, one error object, both doors: { code: 'DUPLICATE_RECORD', status: 409 } // no `name`, so no bespoke arm mapDataError(err, 'duly_note') 409 {"error":"…","code":"DUPLICATE_RECORD","object":"duly_note"} sendThrownError(res, err, 'duly_note') 409 {"error":"…","code":"DUPLICATE_RECORD"} The 5xx arm deliberately gains nothing: its sibling `declaredServerFaultAnswer` names no object either, so that band already agreed and adding the limb there would create the divergence this one removes. The message-text sniff is not lifted above the passthrough. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * test(rest): the residue pin this card closes now states full convergence `error-response-structured-arm-door-parity.test.ts` §4's `ERR_DATASOURCE_UNAVAILABLE` case pinned the bodies differing by one key and named this card as the owner of that residue. With the limb added the case's own CONVERGED label holds for the body too, so the pin asserts door-to-door body equality instead of describing a gap that is closed. The §5 drift guard's `RECORD_NOT_FOUND` entry moves from `known-gap` (citing this card) to `by-design` for the same reason: the arm still stays out of the shared classification — its second limb is a message-TEXT gate, outside the declared-code boundary — but it no longer needs lifting, because both limbs now converge measurably. Leaving a `known-gap` citing a closed card would leave the guard green by disclosing a gap that no longer exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * chore(changeset): raise the rest bump to minor — an additive widening of a published surface The bump rule lives in `.github/workflows/pr-automation.yml`'s Check Changeset prose (and is cited from `scripts/check-changeset-no-major.mjs`), not in AGENTS.md: "A purely additive widening of a published package's public surface (a new exported symbol on an `index`, a new accepted key or value) takes at least `minor`. The commit type may raise a bump but never lower it below what the act requires." Maintainer ruling 2026-09-04, decision batch #35. This PR adds a new accepted key to a published 4xx body, so the act is that widening whatever the `fix(` type says. The historical `patch` precedents this changeset previously cited are named in the same ruling as pre-rule and are deliberately not retro-fixed, so they cannot settle it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6b8c677 commit f5cc78b

4 files changed

Lines changed: 449 additions & 17 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@objectstack/rest": minor
3+
---
4+
5+
fix(rest): the generic declared-status passthrough names its object on both error doors (#14725)
6+
7+
**Response-body change on the published bulk / metadata / UI doors: one optional
8+
key is added, `object`.** Nothing is removed, no status moves, and no `code`
9+
value changes spelling.
10+
11+
#14541 made the two REST error doors agree for every refusal a *bespoke* arm
12+
classifies. They still disagreed for every refusal that reached the *generic*
13+
declared-status passthrough, because the two copies of that one passthrough
14+
differed by exactly one key: `classifyDataError`'s copy ends
15+
`...(object ? { object } : {})` and `resolveErrorResponse`'s 4xx arm had no such
16+
limb. Measured on `main` @ `a12b15e394` — one error object, both doors:
17+
18+
| door | before |
19+
|---|---|
20+
| `mapDataError(err, 'duly_note')` (single-record `/data`) | `409 {"error":"…","code":"DUPLICATE_RECORD","object":"duly_note"}` |
21+
| `sendThrownError(res, err, 'duly_note')` (bulk / metadata / UI) | `409 {"error":"…","code":"DUPLICATE_RECORD"}` |
22+
23+
One refusal, two bodies, decided by which route caught it — the #14541 shape one
24+
arm over. The bulk door now answers the first row too.
25+
26+
It closes the same card's second residue with it. `recordNotFoundError`
27+
(`@objectstack/core`) declares `code`, `status = 404` **and** `object`, so that
28+
declared status carries a record-level not-found past the `RECORD_NOT_FOUND` arm
29+
into this same generic passthrough on every route reporting through
30+
`handleRouteError` / `sendThrownError`, while the single-record `/data` door
31+
reached the generic arm in `classifyDataError` and shipped the name. Both doors
32+
now agree for that producer in every combination of declared status and
33+
door-supplied object.
34+
35+
**Who sees the new key.** The name comes from the door's `object` *argument*,
36+
never from `error.object`, so only a route that supplies one is widened. Of 35
37+
route call sites of this door, **9** pass an argument that can be a non-empty
38+
object name — `POST /data/:object/batch`, `/createMany`, `/updateMany`,
39+
`/deleteMany`, `POST /data/:object/:id/clone`, `POST /data/:object/import`,
40+
`POST /data/:object/import/jobs`, `GET /data/:object/export`, and
41+
`GET /ui/view/:object/:type`. The other 26 (21 passing nothing, 5 passing the
42+
literal `''`) answer byte-identical bodies. `classifiedRefusalAnswer` — the
43+
entry point the analytics dataset face and the record-share family re-dress —
44+
calls this door with no `object` argument at all, so those envelopes' key sets
45+
do not move.
46+
47+
**What deliberately does not change.** The declared-**5xx** arm gains nothing:
48+
its sibling `declaredServerFaultAnswer` names no object either, so the two doors
49+
already agreed in that band and adding the limb there would *create* a
50+
divergence, on top of putting a caller-supplied name into a body whose whole
51+
rule is that a declared server fault says nothing beyond status and code. The
52+
`RECORD_NOT_FOUND` arm's message-**text** limb
53+
(`/^Record \S+ not found in \S+/i`) is not lifted above the passthrough either —
54+
that boundary is #14541's, and it is now pinned behaviourally and positionally
55+
rather than described.
56+
57+
Consumer note: a client that key-counts or exact-matches an error body from a
58+
bulk, import, export, clone or UI-view route will see `object` alongside `error`
59+
and `code` where the equivalent single-record `/data` response has carried it all
60+
along. A client that reads named fields is unaffected.

0 commit comments

Comments
 (0)