Skip to content

Commit 0e1311d

Browse files
refactor(runtime): errorFromThrown builds its answer from the one types composition (#31437)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X1nhftM2hZHHvqk2GLFyVTN)_ domain:server#3 · os-project-manager · session_01FEkQ5nBSzc6ktJyjdFcDb3 Fixes #31319 Clause-②: no (fold onto one composition, no wire change) Metadata-protocol: none ## 维护者速读 - 改了什么:runtime 分发器对「抛出的错误」组装回答的函数 `HttpDispatcher.errorFromThrown`,不再自己逐项拼(封闭 code、引擎唯一约束的线上拼写与上下文、降级的 `declaredCode`、`userMessage`、5xx 泄露文本收回),改为调用 `@objectstack/types` 新导出的 `thrownAnswerOf`;`/actions` 门的读者 `actionsDoorAnswerOf` 的两个分支也用它。只剩一份组装。 - 为什么改:#31315 之后同一个回答有两份组装,今天一致,但没有任何东西把它们绑在一起,改一边另一边会悄悄落后(#31270–#31272 收口的就是这一类漂移)。 - 风险与回滚:线上回答不变:工单点名的四个分发器 pin(`http-dispatcher.error-leak`、`dispatcher-5xx-demoted-code-withhold`、`package-door-error-parity`、`error-envelope.conformance`)零改动全绿,runtime 全量测试全绿。`@objectstack/types` 多一个公开导出。回滚即还原本 PR。 - 先看:`packages/runtime/src/http-dispatcher.ts` 的 `errorFromThrown`(由约 40 行缩为一次调用),`packages/types/src/actions-door-answer.ts` 的 `thrownAnswerOf`,`packages/runtime/src/dispatcher-thrown-answer-tie.test.ts`。 - 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run src/dispatcher-thrown-answer-tie.test.ts`(先 build `@objectstack/types`),预期 7 条全绿。 - 需要你做的:无,席位落地(新增一个公开导出,落地前按惯例做一次契约复审)。 ## What changes - **`@objectstack/types`: new `thrownAnswerOf(err, fallbackStatus)` and its `ThrownAnswer` type** (`src/actions-door-answer.ts`). It is the body of the old private `thrownAnswer` (arms 3 and 7 of `actionsDoorAnswerOf`), lifted out without the door's operator lines: `status`, the closed `code` (the engine duplicate's ruled wire spelling read off `structuredCodeAnswer`), `message` (leaky 5xx withheld), `declaredCode` (`demotedDeclaredCode`), `userMessage`, `details` (resolver `issues` / `fields` plus the duplicate arm's agreeing context). `actionsDoorAnswerOf`'s arms 3 and 7 now return `{ ...thrownAnswerOf(err, fallback), operatorLines }`; its answers are unchanged. - **`@objectstack/runtime` `HttpDispatcher.errorFromThrown`**: builds `declaredCode` / `userMessage` extra from `thrownAnswerOf` and hands the answer to `this.error`, which stays the one construction point for the envelope. The private `duplicateArmContext` helper is removed with its only caller (its job is the `armContext` term inside `thrownAnswerOf`), and the now-unused imports go with it. The method's docblock keeps its history; its one `{@link duplicateArmContext}` now names `thrownAnswerOf`. ## Tests - New `packages/runtime/src/dispatcher-thrown-answer-tie.test.ts` (7 rows), the tie pin. Each row throws the same value at the real `/actions` door (`HttpDispatcher.handleActions`) and at the dispatcher exit (`errorFromThrown`, at the fallback the door's arm reads: 500 for a declared status, 400 for a rejection) and asserts equal `{ status, body }`, then states the term the row carries: the engine duplicate (`UNIQUE_VIOLATION` + `object` / `field` context), a producer merely speaking `DUPLICATE_RECORD`, an unregistered code demoted to `declaredCode`, a leaky declared 503 withheld, a `userMessage`, `issues`, and a record `ValidationError`'s `fields`. - After the fold the two exits cannot drift by construction (one function), so the pin's job is to go red if a later change re-composes either exit by hand. Ablation below shows it does. - The four dispatcher pins the card names pass with zero edits. ## Acceptance notes - **Why a pin as well as the fold.** The fold makes drift between these two exits structurally impossible today. The pin is the cheap guard against a future edit that re-inlines one term at one exit; it costs one file. - **Not folded: `dispatcher-plugin`'s `errorResponseBase`** (the exit for a throw that LEAVES `dispatch()`), whose docblock already declares that it does not carry the engine-duplicate translation. Out of this card's surface; unchanged. - **Worktree name.** The claim names `objectstack-issue-31319`; this ran in the chain's existing worktree `objectstack-issue-31317` on the same claimed branch. - **Branch.** Rebuilt from fresh `origin/main` after #31397 merged; the merged branch had already been deleted on GitHub, so this is a new branch push, no force. ## Verification Head `b8888585` (base `db502314`, the #31397 merge; `origin/main` `383c3530` merged once before opening). <details><summary>No wire change, suites</summary> - The four dispatcher pins the card names, unedited, after the fold: `http-dispatcher.error-leak.test.ts`, `dispatcher-5xx-demoted-code-withhold.test.ts`, `package-door-error-parity.test.ts`, `error-envelope.conformance.test.ts` with the new tie pin: Test Files 5 passed (5), Tests 118 passed (118). - `@objectstack/types` vitest: Test Files 38 passed (38), Tests 881 passed (881). - `@objectstack/runtime` vitest (types from the rebuilt dist): Test Files 560 passed (560), Tests 10323 passed, 19 skipped; lock VERDICT command-exit 0. No existing pin edited. - `typecheck` for `@objectstack/types` and `@objectstack/runtime`: exit 0. `eslint --no-inline-config packages/types packages/runtime`: exit 0. - `@objectstack/mcp` not re-run: its source is untouched, `actionsDoorAnswerOf`'s answers are unchanged and the types change is an added export. </details> <details><summary>Ablation of the tie pin (ablation-replace, restored to HEAD)</summary> - `packages/runtime/src/http-dispatcher.ts`: `errorFromThrown` made to drop the `userMessage` term (`&& String("ABL31319").length < 0` on its spread), a hand-made drift at one exit. Mutation landed (anchor 1 to 0, blob fce6d0b to 25ec094ea41b). `dispatcher-thrown-answer-tie.test.ts`: Tests 1 failed | 6 passed (7), the `userMessage` row, `AssertionError: expected { status: 409, body: { …(2) } } to deeply equal { status: 409, body: { …(2) } }`. The runtime suite reads its own `src/`, so no dist leg. - Restore: blob == HEAD (fce6d0b), `git diff HEAD` empty, `git status --porcelain` empty. </details> <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `b8888585`: 68 commands, each run with `timeout 1500` and stdin from `/dev/null`, exit codes captured before any pipe, all exit 0 on the first run (after a `@objectstack/runtime` closure build). `dispatch-gates --ran`: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN. Plus the typechecks, scoped eslint and the two full suites above. Not run locally (CI): `test:repo`, dogfood shards, type-check lanes. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5nBSzc6ktJyjdFcDb3)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent deac7f4 commit 0e1311d

4 files changed

Lines changed: 216 additions & 89 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/types': patch
3+
'@objectstack/runtime': patch
4+
---
5+
6+
refactor(runtime): the dispatcher answers a thrown value through the one composition the `/actions` door reads
7+
8+
Clause-②: no (fold onto one composition, no wire change)
9+
Metadata-protocol: none
10+
11+
`HttpDispatcher.errorFromThrown` (`@objectstack/runtime`), the exit every dispatcher domain answers a thrown value through, composed its answer term by term beside the `/api/v1/actions` door's reader (`actionsDoorAnswerOf`, `@objectstack/types`): the resolver's closed `code`, the engine duplicate's wire spelling and context, the demoted `declaredCode`, the declared `userMessage`, and the 5xx leak withhold. The two agreed, and nothing tied them. Both now build their answer from one function, so a change to it moves every door that answers a throw this way.
12+
13+
**New export (`@objectstack/types`): `thrownAnswerOf(err, fallbackStatus)` and its `ThrownAnswer` type**, the runtime dispatcher's answer to a thrown value at a fallback status: `status`, `code`, `message`, and `declaredCode`, `userMessage`, `details` when present. `actionsDoorAnswerOf`'s declared-status and rejection arms return it with the door's operator lines.
14+
15+
No response changes: the dispatcher's existing envelope pins pass unedited.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#31319] ONE composition of the answer to a thrown value: the runtime
5+
* dispatcher's exit (`HttpDispatcher.errorFromThrown`, every dispatcher
6+
* domain's catch through `deps.errorFromThrown`) and the `/api/v1/actions`
7+
* door (`actionsDoorAnswerOf`'s declared-status and rejection arms) both build
8+
* their envelope from `thrownAnswerOf` (`@objectstack/types`).
9+
*
10+
* The tie: for each throw below, the dispatcher's exit at the fallback the
11+
* `/actions` door would read (500 for a throw that declared a status, 400 for
12+
* a rejection) answers the SAME response, status and body, as the door. A
13+
* door that composed its own copy and let one term drift (the engine
14+
* duplicate's wire code or context, the demoted `declaredCode`, the
15+
* `userMessage`, the leaky 5xx withhold, the structured `details`) reds the
16+
* row that carries that term. Each row also states the term it carries, so
17+
* the two cannot agree on something nobody wrote down.
18+
*/
19+
20+
import { describe, it, expect, vi, afterEach } from 'vitest';
21+
// The dispatcher's reach loads this lazily; pay its first load during collection.
22+
import '@objectstack/metadata';
23+
import { DuplicateRecordError } from '@objectstack/objectql';
24+
import { INTERNAL_ERROR_MESSAGE } from '@objectstack/types';
25+
26+
import { HttpDispatcher } from './http-dispatcher.js';
27+
28+
const OBJECT = 'crm_invoice';
29+
30+
/** A dispatcher whose `submit_signoff` action handler rejects with `thrown`. */
31+
function makeDispatcher(thrown: unknown) {
32+
const objectDef = {
33+
name: OBJECT,
34+
actions: [{ name: 'submit_signoff', objectName: OBJECT, type: 'script', body: { language: 'js', source: 'return 1;' } }],
35+
};
36+
const ql: any = {
37+
executeAction: vi.fn(async () => { throw thrown; }),
38+
getSchema: (n: string) => (n === objectDef.name ? objectDef : undefined),
39+
registry: { getObject: (n: string) => (n === objectDef.name ? objectDef : undefined), getItem: () => undefined },
40+
find: vi.fn(async () => [{ id: 'inv_1' }]),
41+
insert: vi.fn(), update: vi.fn(), delete: vi.fn(),
42+
};
43+
const metadata: any = {
44+
load: vi.fn(async () => null),
45+
listObjects: vi.fn(async () => [objectDef]),
46+
getObject: vi.fn(async () => objectDef),
47+
};
48+
const kernel: any = {
49+
context: { getService: (n: string) => (n === 'objectql' || n === 'data' ? ql : n === 'metadata' ? metadata : null) },
50+
};
51+
return new HttpDispatcher(kernel);
52+
}
53+
54+
/** The `/actions` door's response to an action body that threw `thrown`. */
55+
async function actionsDoor(thrown: unknown): Promise<{ status: number; body: any }> {
56+
const res: any = await makeDispatcher(thrown).handleActions(
57+
`/${OBJECT}/submit_signoff/inv_1`,
58+
'POST',
59+
{},
60+
{ request: {}, environmentId: 'platform', executionContext: { userId: 'u1', systemPermissions: [] } } as any,
61+
);
62+
return res.response;
63+
}
64+
65+
/** The dispatcher's exit for the same throw, at `fallbackStatus`. */
66+
function dispatcherExit(thrown: unknown, fallbackStatus: number): { status: number; body: any } {
67+
return (makeDispatcher(undefined) as any).errorFromThrown(thrown, fallbackStatus);
68+
}
69+
70+
afterEach(() => {
71+
vi.restoreAllMocks();
72+
});
73+
74+
const ROWS: ReadonlyArray<{ term: string; fallback: number; thrown: () => unknown; carries: (error: any) => void }> = [
75+
{
76+
term: 'the engine duplicate: the ruled wire code and the REST arm\'s context',
77+
fallback: 500,
78+
thrown: () => new DuplicateRecordError(OBJECT, new Error('driver refusal'), 'email'),
79+
carries: (error) => {
80+
expect(error.code).toBe('UNIQUE_VIOLATION');
81+
expect(error.details).toMatchObject({ object: OBJECT, field: 'email' });
82+
},
83+
},
84+
{
85+
term: 'a producer that merely speaks DUPLICATE_RECORD keeps its own code',
86+
fallback: 500,
87+
thrown: () => Object.assign(new Error('That address is already on file'), { code: 'DUPLICATE_RECORD', status: 409 }),
88+
carries: (error) => expect(error.code).toBe('DUPLICATE_RECORD'),
89+
},
90+
{
91+
term: 'an unregistered code with a declared status: the closed code, the spelling demoted',
92+
fallback: 500,
93+
thrown: () => Object.assign(new Error('No leave days left.'), { code: 'LEAVE_EXHAUSTED', status: 409 }),
94+
carries: (error) => {
95+
expect(error.code).toBe('RESOURCE_CONFLICT');
96+
expect(error.declaredCode).toBe('LEAVE_EXHAUSTED');
97+
},
98+
},
99+
{
100+
term: 'a declared 5xx with leaky prose: the sentence withheld',
101+
fallback: 500,
102+
thrown: () => Object.assign(new Error('SQLITE_ERROR: no such table: crm_invoice_x'), { status: 503 }),
103+
carries: (error) => expect(error.message).toBe(INTERNAL_ERROR_MESSAGE),
104+
},
105+
{
106+
term: 'a declared userMessage rides beside the code',
107+
fallback: 500,
108+
thrown: () => Object.assign(new Error('Locked by finance.'), { code: 'RECORD_LOCKED', status: 409, userMessage: 'Ask finance to unlock it.' }),
109+
carries: (error) => {
110+
expect(error.code).toBe('RECORD_LOCKED');
111+
expect(error.userMessage).toBe('Ask finance to unlock it.');
112+
},
113+
},
114+
{
115+
term: 'a rejection with structured issues: VALIDATION_ERROR, issues in details',
116+
fallback: 400,
117+
thrown: () => Object.assign(new Error('Spec invalid.'), { issues: [{ path: ['name'], message: 'Required', code: 'invalid_type' }] }),
118+
carries: (error) => {
119+
expect(error.code).toBe('VALIDATION_ERROR');
120+
expect(error.details.issues).toHaveLength(1);
121+
},
122+
},
123+
{
124+
term: 'a record ValidationError: VALIDATION_FAILED, fields in details',
125+
fallback: 400,
126+
thrown: () => Object.assign(new Error('Validation failed'), { name: 'ValidationError', fields: [{ field: 'amount', message: 'Must be positive' }] }),
127+
carries: (error) => {
128+
expect(error.code).toBe('VALIDATION_FAILED');
129+
expect(error.details.fields).toEqual([{ field: 'amount', message: 'Must be positive' }]);
130+
},
131+
},
132+
];
133+
134+
describe('[#31319] the dispatcher exit and the /actions door answer a throw through one composition', () => {
135+
for (const row of ROWS) {
136+
it(row.term, async () => {
137+
vi.spyOn(console, 'error').mockImplementation(() => {});
138+
const door = await actionsDoor(row.thrown());
139+
const exit = dispatcherExit(row.thrown(), row.fallback);
140+
expect(exit).toEqual({ status: door.status, body: door.body });
141+
row.carries(exit.body.error);
142+
});
143+
}
144+
});

‎packages/runtime/src/http-dispatcher.ts‎

Lines changed: 15 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
AuthzStoreUnavailableError,
1818
type AuthGate,
1919
} from '@objectstack/core';
20-
import { isMcpServerEnabled, looksLikeInternalErrorLeak, INTERNAL_ERROR_MESSAGE, resolveThrownHttpError, demotedDeclaredCode, declaredUserMessage, inProcessSessionReadInput, isEngineDuplicateRecordEnvelope, structuredCodeAnswer, driverNotNullRefusalAnswer } from '@objectstack/types';
20+
import { isMcpServerEnabled, looksLikeInternalErrorLeak, INTERNAL_ERROR_MESSAGE, declaredUserMessage, inProcessSessionReadInput, driverNotNullRefusalAnswer, thrownAnswerOf } from '@objectstack/types';
2121
import { measureServerTiming, allowPerfDisclosure, isPerfDisclosurePrincipal } from '@objectstack/observability';
2222
import { CoreServiceName, serviceUnavailableMessage, inProcessServiceMessage } from '@objectstack/spec/system';
2323
import type { AuthSessionApi, IDataEngine, IObjectQLEngine } from '@objectstack/spec/contracts';
@@ -391,43 +391,6 @@ type ClassifiedServiceLookup =
391391
| { readonly outcome: 'never-registered' }
392392
| { readonly outcome: 'no-instance-in-scope'; readonly scopeId: string };
393393

394-
/**
395-
* [#23686] The structured context `RestServer`'s duplicate-record arm states
396-
* beside its `UNIQUE_VIOLATION`, for the dispatcher envelope's `details`.
397-
* Asked only for the engine's own `DuplicateRecordError` (see
398-
* `HttpDispatcher.errorFromThrown`, "One wire spelling").
399-
*
400-
* ONE classification, not a third spelling: the fields are read off the arm
401-
* `structuredCodeAnswer` (`@objectstack/types`) answers the same throw
402-
* through, so this door carries what the REST door and the Hono doors carry,
403-
* each field only when the producer stated it, and an arm that grows a field
404-
* grows it here too. ⛔ Never list the fields here. The arm's own sentence
405-
* (`error`) and `code` are dropped because the envelope already carries both,
406-
* as `message` and `code`.
407-
*
408-
* The arm must AGREE with the envelope already decided, as at the Hono doors:
409-
* its status must be the thrown one and its code the envelope's, so it can
410-
* only add context, never change the answer. Disagreeing, it adds nothing.
411-
* The envelope's code for this class is itself read off the same arm
412-
* (`HttpDispatcher.errorFromThrown`, #23714), so this function spells no copy
413-
* of it either.
414-
*
415-
* ⛔ Deliberately not every arm. The dispatcher's other 409s
416-
* (`CONCURRENT_UPDATE`, `DELETE_RESTRICTED`) keep the body they have; widening
417-
* the context this door carries for them is a change of its own.
418-
*/
419-
function duplicateArmContext(
420-
thrown: unknown,
421-
envelope: { status: number; code: string },
422-
): Record<string, unknown> | undefined {
423-
const arm = structuredCodeAnswer(thrown);
424-
if (arm === undefined || arm.status !== envelope.status || arm.body.code !== envelope.code) return undefined;
425-
const context = Object.fromEntries(
426-
Object.entries(arm.body).filter(([key]) => key !== 'error' && key !== 'code'),
427-
);
428-
return Object.keys(context).length > 0 ? context : undefined;
429-
}
430-
431394
/**
432395
* The HTTP dispatch engine — translates an inbound (method, path, body, ctx)
433396
* request into a kernel response. Used directly by the framework's HTTP adapters
@@ -1240,7 +1203,7 @@ export class HttpDispatcher {
12401203
* registered code without being the engine's class) keeps its own code
12411204
* and its own body. Beside the code,
12421205
* the envelope's `details` carries what `RestServer`'s arm carries beside
1243-
* its own, read off that one classification ({@link duplicateArmContext})
1206+
* its own, read off that one classification (`thrownAnswerOf`, `@objectstack/types`, #31319)
12441207
* rather than listed here: `developerMessage`, `object`, and `field` when
12451208
* the driver named the column. `message` stays the thrown one, as on every
12461209
* other code at this door and at the Hono doors; `declaredCode` stays
@@ -1253,60 +1216,26 @@ export class HttpDispatcher {
12531216
* divergence is declared rather than closed by a second translator.
12541217
*/
12551218
private errorFromThrown(e: any, fallbackStatus = 500) {
1256-
const thrown = resolveThrownHttpError(e, fallbackStatus);
1257-
// [#23686] The code this envelope carries: the resolver's, except the
1258-
// engine's unique-constraint envelope, which crosses HTTP as the one
1259-
// ruled wire spelling (see "One wire spelling" above).
1260-
//
1261-
// [#23714] That wire code is read off the shared classification's arm
1262-
// for the class (`structuredCodeAnswer`, the arm `RestServer` answers
1263-
// with), so this package spells no copy of it: the stamp stays where
1264-
// the error-code ledger lists it, under `@objectstack/types`. The
1265-
// resolver's code is kept only if that arm ever answered no string
1266-
// code for the class.
1267-
const engineDuplicate = isEngineDuplicateRecordEnvelope(e);
1268-
const duplicateArmCode = engineDuplicate ? structuredCodeAnswer(e)?.body.code : undefined;
1269-
const code = typeof duplicateArmCode === 'string' ? duplicateArmCode : thrown.code;
1270-
const armContext = engineDuplicate ? duplicateArmContext(e, { status: thrown.status, code }) : undefined;
1271-
const details = armContext ? { ...thrown.details, ...armContext } : thrown.details;
1272-
// [#9106] `code`, the NARROWED spelling — the same one the REST door
1273-
// has served since #8016, so the two doors now agree on `error.code`
1274-
// unconditionally (maintainer ruling 2026-08-16: `error.code` is a
1275-
// closed vocabulary at every door). A producer's unregistered spelling
1276-
// is not dropped: it rides the wire's `declaredCode`
1277-
// (`ApiErrorSchema.declaredCode`, the open author-authored channel),
1278-
// which is how a metadata app's own thrown code — the #7867 capability,
1279-
// measured as the tenant-authored limb no ledger can enumerate — still
1280-
// reaches the wire. Emitted only when the demote actually happened, so
1281-
// presence means demotion (see `demotedDeclaredCode`'s note).
1282-
//
1283-
// [#8087] The producer sweep is unchanged by this: platform producers
1284-
// must still register — `./dispatcher-error-vocabulary.ts` carries the
1285-
// classified list and `pnpm check:dispatcher-error-vocabulary` fails on
1286-
// an unswept producer, whose semantic code would otherwise silently
1287-
// demote off the wire. `error-envelope.conformance.test.ts` drives
1288-
// every dispatcher-reachable member of that list through this method
1289-
// and parses the body against `ApiErrorSchema`, which now PASSES for
1290-
// every body this door emits — the "parse every body it emits" half.
1291-
const declaredCode = demotedDeclaredCode(thrown);
1292-
// [commit 79c46da90] The producer-side user-facing marking rides as a declared
1293-
// sibling of `code`/`message` (`ApiErrorSchema.userMessage`), exactly
1294-
// like `declaredCode` — the shared resolver already answered whether
1295-
// the throw declared one (`declaredUserMessage`'s non-empty-string
1296-
// rule), so this door and the REST door agree by construction. Note
1297-
// `this.error`'s 5xx leak-withholding touches only `message`; the
1298-
// marked channel is authored user text, never the withheld prose.
1219+
// [#31319] The answer is composed ONCE, by `thrownAnswerOf`
1220+
// (`@objectstack/types`), the function the `/actions` door's reader
1221+
// (`actionsDoorAnswerOf`) answers its declared-status and rejection
1222+
// arms with. Every term the notes above describe (the resolver's
1223+
// closed code, the engine duplicate's wire spelling and arm context,
1224+
// the demoted `declaredCode`, the declared `userMessage`, the 5xx leak
1225+
// withhold) is that function's, so no door answering a thrown value
1226+
// carries a copy that could drift from the others. `this.error` stays
1227+
// the one construction point for the envelope.
1228+
const answer = thrownAnswerOf(e, fallbackStatus);
12991229
const extra = {
1300-
...(declaredCode !== undefined ? { declaredCode } : {}),
1301-
...(thrown.userMessage !== undefined ? { userMessage: thrown.userMessage } : {}),
1230+
...(answer.declaredCode !== undefined ? { declaredCode: answer.declaredCode } : {}),
1231+
...(answer.userMessage !== undefined ? { userMessage: answer.userMessage } : {}),
13021232
};
13031233
return this.error(
1304-
thrown.message, thrown.status, details, code,
1234+
answer.message, answer.status, answer.details, answer.code,
13051235
Object.keys(extra).length > 0 ? extra : undefined,
13061236
);
13071237
}
13081238

1309-
13101239
/**
13111240
* 404 Route Not Found — no route is registered for this path.
13121241
*

0 commit comments

Comments
 (0)