Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -444,7 +444,10 @@ describe('[#5352] reading the envelope did not turn every failure into a 400', (
});

it('a HALF envelope (4xx status, no code) is not honoured — this route invents no code', async () => {
// ADR-0112's point is that the PRODUCER names the condition. A status with
// ADR-0112 D4's point is the semantic-CODE channel: `error.code` is closed
// at every door and the producer's own spelling rides `declaredCode`
// beside it — the producer names the condition ON THE CODE AXIS. The ADR
// rules no HTTP status for an undeclared throw. A status with
// no code is a producer bug; answering it with a code chosen here would be
// the consumer-side leniency the ADR exists to remove, and would hide the
// bug behind a plausible wire shape.
Expand Down
24 changes: 20 additions & 4 deletions packages/rest/src/error-response.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -391,8 +391,19 @@ function declaredHttpStatus(error: any): number | undefined {
* of `declaredCode` means demotion, exactly as `ApiErrorSchema.declaredCode`
* documents for the nested envelope.
* - the producer spelled NO string code → `{}`. Nothing is invented: ADR-0112
* says the PRODUCER names the condition, so a half-declaration is honoured
* for the half that was declared. That is the answer this file already gave
* D4 governs the semantic-CODE channel — `error.code` closed at every door,
* the producer's own spelling honoured beside it as `declaredCode` — so a
* half-declaration is honoured for the half that was DECLARED.
*
* ⚠️ Axis, stated once here because the other sites echo this file: the
* ADR rules the CODE. It does not rule what HTTP status an undeclared
* throw deserves, and "the PRODUCER names the condition" is this file's
* own prose for the code rule — the sentence does not appear in
* ADR-0112 and must not be cited as though it ruled the status axis.
* Read unqualified it was taken for a status ruling once already, and
* argued for a `500` on a contract question the ADR is silent on.
*
* That is the answer this file already gave
* (see the 5xx arm of {@link mapDataError}) and it is deliberately preserved
* — narrowing the vocabulary must not start ADDING codes to bodies that
* carried none.
Expand DownExpand Up@@ -920,9 +931,14 @@ function classifyDataError(error: any, object?: string): { status: number; body:
// empty string from landing on the wire as an ADR-0112 code.
//
// A 5xx with NO code passes its status through carrying no code at all,
// deliberately: ADR-0112 says the PRODUCER names the condition, so a
// deliberately: ADR-0112 D4 governs the semantic-CODE channel — the
// producer names the condition on the CODE axis (see the
// `resolveThrownHttpError` docblock above for why the phrase is this
// file's own prose and not an ADR quotation) — so a
// half-declaration is honoured for the half that was declared and
// nothing is invented for the half that was not. That is the answer
// nothing is invented for the half that was not. The status half is
// kept on #5582/#7525's rule, not the ADR's: ADR-0112 rules no HTTP
// status here. That is the answer
// `resolveErrorResponse` already gives the same shape
// (`rest-5xx-message-sanitization.test.ts` §"a dynamically-assigned
// status is treated identically"), and inventing `INTERNAL_ERROR` here
Expand Down
6 changes: 4 additions & 2 deletions packages/rest/src/rest-5xx-message-sanitization.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -379,8 +379,10 @@ describe('[#5437] the 5xx envelope: status and code survive, the prose does not'

expect(res.statusCode).toBe(500);
expect(res.body.error).toBe(INTERNAL_ERROR_MESSAGE);
// No `code` was declared, so none is invented here — ADR-0112 says the
// PRODUCER names the condition.
// No `code` was declared, so none is invented here — ADR-0112 D4
// governs the semantic-CODE channel (the producer names the condition
// on the CODE axis; the ADR rules no HTTP status for an undeclared
// throw).
expect(res.body.code).toBeUndefined();
expect(loggedText('does not exist')).toBe(true);
}, 60_000);
Expand Down
9 changes: 6 additions & 3 deletions packages/rest/src/rest-5xx-status-passthrough.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,9 +214,12 @@ describe('[#5582] a 5xx that declares no code passes its status and invents noth
// This is the exact shape the two pins this issue named were written
// on, and the exact shape `resolveErrorResponse` already answers this
// way ("a dynamically-assigned status is treated identically (no code
// declared)", `rest-5xx-message-sanitization.test.ts`): ADR-0112 says
// the PRODUCER names the condition, so the half that was declared is
// honoured and the half that was not is left empty. Inventing
// declared)", `rest-5xx-message-sanitization.test.ts`): ADR-0112 D4
// governs the semantic-CODE channel — the producer names the condition
// on the CODE axis — so the half that was declared is
// honoured and the half that was not is left empty. The 502 itself
// passes through on #5437/#5582's rule, NOT the ADR's: ADR-0112 rules
// no HTTP status for an undeclared throw. Inventing
// `INTERNAL_ERROR` here would put a code on the wire nobody wrote —
// and overwriting the 502 with a 500 would re-derive a declared status
// from message text, which is what #5437 ruled against one door over.
Expand Down
4 changes: 3 additions & 1 deletion packages/rest/src/rest-hook-refusal-code-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -343,7 +343,9 @@ describe('[#10345] the sandbox unwrap is the branch, and it was never status-sha

describe('[#10345] the pinned defaults the fix must not disturb', () => {
it('a refusal that declares NO code still carries none — nothing is invented', () => {
// ADR-0112: the PRODUCER names the condition. Narrowing or widening the
// ADR-0112 D4 governs the semantic-CODE channel — the producer names
// the condition on the CODE axis, and the ADR rules no HTTP status for
// an undeclared throw. Narrowing or widening the
// vocabulary must never start ADDING codes to bodies that carried none.
const r = mapDataError(sandboxRefusal('month-end close is in progress'), 'crm_account');
expect(r.status).toBe(400);
Expand Down
15 changes: 12 additions & 3 deletions packages/rest/src/rest-hook-refusal-status-passthrough.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -380,9 +380,18 @@ describe('[#7525] a hook that refuses WITHOUT a status is unchanged', () => {

expect(r.status).toBe(500);
expect(r.body.code).toBe('INTERNAL_ERROR');
// Its own `code` is NOT promoted to the wire by this fix: ADR-0112 says
// the producer names the condition, and this producer named a code but
// no status. Inventing a 4xx from the code alone would be the
// Its own `code` is NOT promoted to the wire by this fix: ADR-0112 D4
// governs the semantic-CODE channel — the producer names the condition
// on the CODE axis — and this producer named a code but
// no status.
//
// ⚠️ Do not read the 500 above as an ADR-0112 ruling. It is the
// classifiers' default (#7525); ADR-0112 rules no HTTP status for an
// undeclared throw. The unqualified form of this sentence was read as
// ruling exactly that, and argued for a `500` on the sibling
// `/analytics/dataset/query` question the ADR is silent on.
//
// Inventing a 4xx from the code alone would be the
// consumer-side leniency Prime Directive #12 removes — the follow-up
// that belongs with #7463, not here.
expect(r.body.code).not.toBe('CLOSE_PERIOD_LOCKED');
Expand Down
4 changes: 3 additions & 1 deletion packages/rest/src/rest-thrown-code-vocabulary.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -250,7 +250,9 @@ describe('#9232 §3 — the halves that must not move', () => {
expect(status).toBe(arm.status);
expect(body.code).toBeUndefined();
expect(body.declaredCode).toBeUndefined();
// ADR-0112 says the PRODUCER names the condition, so a
// ADR-0112 D4 governs the semantic-CODE channel — the producer
// names the condition on the CODE axis, and the ADR rules no HTTP
// status for an undeclared throw — so a
// half-declaration is honoured for the half that was declared.
// Narrowing the vocabulary must not start ADDING codes to bodies
// that carried none.
Expand Down
9 changes: 7 additions & 2 deletions packages/spec/src/shared/external-errors.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,8 +44,13 @@ export type ExternalErrorCode =
*
* ## Why HERE and not in a REST error map
*
* ADR-0112: the PRODUCER names the condition. This repo's HTTP exits already
* agree on how to read one — `status` then `statusCode`, 400-599 — in
* The producer that knows the condition declares its own wire shape. For the
* STATUS half — which is what this table decides — that is this repo's
* convergent HTTP-exit convention, NOT an ADR-0112 ruling: ADR-0112 governs
* the semantic-CODE channel (cited correctly above for the ledgered `code`,
* D3) and rules no HTTP status for a throw. This repo's HTTP exits already
* agree on how to read a declared status — `status` then `statusCode`,
* 400-599 — in
* `mapDataError`'s `declaredHttpStatus` (#7525), `resolveErrorResponse`
* (#5437/#5582), `HttpDispatcher.errorFromThrown` (#3867),
* `dispatcher-plugin.errorResponseBase`, `endpoint-executor`,
Expand Down
Loading