@procedures/client 0.35.0 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/server/handler.d.ts.map +1 -1
- package/dist/adapters/http/server/handler.js +17 -3
- package/dist/adapters/http/server/handler.js.map +1 -1
- package/dist/adapters/port/transport.d.ts.map +1 -1
- package/dist/adapters/port/transport.js +2 -0
- package/dist/adapters/port/transport.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +2 -0
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.js +14 -5
- package/dist/adapters/websocket/shared/rpc.js.map +1 -1
- package/dist/browser.d.ts +3 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -1
- package/dist/browser.js.map +1 -1
- package/dist/client/errors/index.d.ts +1 -1
- package/dist/client/errors/index.d.ts.map +1 -1
- package/dist/client/errors/index.js +1 -1
- package/dist/client/errors/index.js.map +1 -1
- package/dist/client/errors/registry.d.ts +10 -0
- package/dist/client/errors/registry.d.ts.map +1 -1
- package/dist/client/errors/registry.js +12 -0
- package/dist/client/errors/registry.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/procedures/collection/changes.d.ts +15 -0
- package/dist/procedures/collection/changes.d.ts.map +1 -1
- package/dist/procedures/collection/changes.js.map +1 -1
- package/dist/server/error-policy.d.ts +96 -0
- package/dist/server/error-policy.d.ts.map +1 -0
- package/dist/server/error-policy.js +196 -0
- package/dist/server/error-policy.js.map +1 -0
- package/dist/server/error-response.d.ts +30 -0
- package/dist/server/error-response.d.ts.map +1 -0
- package/dist/server/error-response.js +90 -0
- package/dist/server/error-response.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/last-resort.d.ts +17 -0
- package/dist/server/last-resort.d.ts.map +1 -0
- package/dist/server/last-resort.js +19 -0
- package/dist/server/last-resort.js.map +1 -0
- package/dist/server/procedure-server.d.ts +3 -11
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +7 -42
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/server.d.ts +43 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +39 -2
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +12 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/http/server/handler.ts +18 -3
- package/src/adapters/port/transport.ts +2 -0
- package/src/adapters/websocket/server/transport.ts +2 -0
- package/src/adapters/websocket/shared/rpc.ts +14 -5
- package/src/browser.ts +4 -0
- package/src/client/errors/index.ts +1 -0
- package/src/client/errors/registry.ts +13 -0
- package/src/client/index.ts +1 -0
- package/src/procedures/collection/changes.ts +12 -0
- package/src/server/error-policy.ts +263 -0
- package/src/server/error-response.ts +91 -0
- package/src/server/index.ts +2 -0
- package/src/server/last-resort.ts +23 -0
- package/src/server/procedure-server.ts +8 -41
- package/src/server/server.ts +73 -3
- package/src/server/types.ts +13 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error policy of a server (roadmap CLI-80): what an error tells the caller when it leaves
|
|
3
|
+
* the process.
|
|
4
|
+
*
|
|
5
|
+
* `Server.handle()` applies the policy to each response, so each face of the server (HTTP,
|
|
6
|
+
* WebSocket, MCP, a message port, the local face of a host) sends the same error. When
|
|
7
|
+
* `handle()` throws, or a face fails after it (for example, a value that JSON cannot encode), the
|
|
8
|
+
* last-resort catch of the face applies the same policy through `lastResortError`. A nested call
|
|
9
|
+
* of `ctx.client` does not go through `handle()`, so it keeps the real error.
|
|
10
|
+
*
|
|
11
|
+
* - `expose: "all"` (the default): each error goes to the caller as before.
|
|
12
|
+
* - `expose: "coded"`: an error with a known code (`isKnownErrorCode`, or a code of `codes`) goes
|
|
13
|
+
* to the caller as it is. Each other error goes to the caller as `INTERNAL_ERROR` with the
|
|
14
|
+
* message `"internal error"`, and the server logs it one time.
|
|
15
|
+
* - `map` runs first. Its result goes to the caller as it is. `undefined` gives the error to
|
|
16
|
+
* `expose`.
|
|
17
|
+
*
|
|
18
|
+
* The policy reads the real error of a response through the links of `error-response.ts`: the
|
|
19
|
+
* status object, and the request object with the code and the message of the status. A copy of
|
|
20
|
+
* the response or of its status keeps a link.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ProcedurePath } from "../procedures/types.js";
|
|
24
|
+
import { closableUnread } from "../procedures/invoke.js";
|
|
25
|
+
import { isKnownErrorCode } from "../client/errors/registry.js";
|
|
26
|
+
import { errorCodeOf } from "../protocol.js";
|
|
27
|
+
import { errorResponse, realErrorOf } from "./error-response.js";
|
|
28
|
+
import { methodToPath } from "./method.js";
|
|
29
|
+
import { rawErrorOf } from "./last-resort.js";
|
|
30
|
+
export { lastResortError } from "./last-resort.js";
|
|
31
|
+
import { ServerError, type ServerRequest, type ServerResponse } from "./types.js";
|
|
32
|
+
|
|
33
|
+
/** The error that the caller gets: its code, its message and its retryable flag. A `ServerError` is one. */
|
|
34
|
+
export interface ExposedError {
|
|
35
|
+
/** The error code, for example `NOT_FOUND`, or a code of the application. */
|
|
36
|
+
readonly code: string;
|
|
37
|
+
/** The message that the caller gets. */
|
|
38
|
+
readonly message: string;
|
|
39
|
+
/**
|
|
40
|
+
* True when a retry of the same call can succeed. Without it, the error gets the flag of its
|
|
41
|
+
* code before the first item of a stream, and false after it (`retryableOf`).
|
|
42
|
+
*/
|
|
43
|
+
readonly retryable?: boolean | undefined;
|
|
44
|
+
/** The wait before a retry, in milliseconds, as on an error of a handler (`Retry-After` on HTTP). */
|
|
45
|
+
readonly retryAfterMs?: number | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The call of an error that leaves the server. */
|
|
49
|
+
export interface ErrorCall {
|
|
50
|
+
/** The path of the procedure. Empty when the face failed before it knew the call. */
|
|
51
|
+
readonly path: ProcedurePath;
|
|
52
|
+
/** The face that took the request (`http`, `websocket`, `port`, `mcp`), when the face tells it. */
|
|
53
|
+
readonly transport?: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The error policy of a server: the option `errors` (roadmap CLI-80). */
|
|
57
|
+
export interface ServerErrorPolicy {
|
|
58
|
+
/**
|
|
59
|
+
* Which errors go to the caller as they are:
|
|
60
|
+
* - `"all"` (the default): each error, with its code and its message, as before.
|
|
61
|
+
* - `"coded"`: only an error with a known code (`isKnownErrorCode`: `NOT_FOUND`, `FORBIDDEN`,
|
|
62
|
+
* `VALIDATION_ERROR`, `CONFLICT` and so on, or a code of `codes`), with its code, its
|
|
63
|
+
* message, its retryable flag and its metadata. Each other error goes to the caller as
|
|
64
|
+
* `INTERNAL_ERROR` with the message `"internal error"` and its real retryable flag, with
|
|
65
|
+
* only the metadata keys of `keepMetadata`. The codes `INTERNAL_ERROR`, `HANDLER_ERROR`, `UNKNOWN`,
|
|
66
|
+
* `OUTPUT_VALIDATION_ERROR` and `NO_OUTPUT` tell of a failure of the server, not of a
|
|
67
|
+
* refusal, so their message is hidden too. The server gives the real error to its `onError`
|
|
68
|
+
* logger one time, or to `console.error` without a logger.
|
|
69
|
+
*/
|
|
70
|
+
readonly expose?: "all" | "coded" | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* The codes of the application that also go to the caller as they are under `"coded"`, for
|
|
73
|
+
* example `["QUOTA_EXCEEDED"]`.
|
|
74
|
+
*/
|
|
75
|
+
readonly codes?: readonly string[] | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* The metadata keys that a hidden error response or a `map` result keeps from the response: an
|
|
78
|
+
* allowlist. Each other key goes, also a key that the server does not know (`issues`, a key of
|
|
79
|
+
* the application). The default is `DEFAULT_KEPT_METADATA`: the trace keys `traceparent` and
|
|
80
|
+
* `tracestate`.
|
|
81
|
+
*/
|
|
82
|
+
readonly keepMetadata?: readonly string[] | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* This function runs first, for each error that leaves the server. Its result goes to the
|
|
85
|
+
* caller as it is, also with a code of the application, and with the metadata of an error of a
|
|
86
|
+
* handler (`retryAfterMs`, the conflicts of a `VERSION_CONFLICT` error). `undefined` gives the
|
|
87
|
+
* error to `expose`. A function that throws gives `INTERNAL_ERROR`.
|
|
88
|
+
*/
|
|
89
|
+
readonly map?: ((error: unknown, call: ErrorCall) => ExposedError | undefined) | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The message of a hidden error. */
|
|
93
|
+
export const HIDDEN_ERROR_MESSAGE = "internal error";
|
|
94
|
+
|
|
95
|
+
/** The codes that tell of a failure of the server, not of a refusal: `"coded"` hides their message. */
|
|
96
|
+
const FAILURE_CODES: ReadonlySet<string> = new Set(["INTERNAL_ERROR", "HANDLER_ERROR", "UNKNOWN", "OUTPUT_VALIDATION_ERROR", "NO_OUTPUT"]);
|
|
97
|
+
|
|
98
|
+
/** The errors that a logger got: each path logs an error one time. */
|
|
99
|
+
const LOGGED = new WeakSet<object>();
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* This function records that the logger of the server got this error, so the error policy does
|
|
103
|
+
* not log it again.
|
|
104
|
+
*
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
export function markLogged(error: unknown): void {
|
|
108
|
+
if (typeof error === "object" && error !== null) LOGGED.add(error);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The logger of the hidden errors: the hook `onError` of the server, or `console.error`. */
|
|
112
|
+
export type HiddenErrorLog = (error: unknown, request: ServerRequest<unknown> | undefined) => void;
|
|
113
|
+
|
|
114
|
+
/** This function gives an error to the logger, when the logger did not get it before. */
|
|
115
|
+
function logOnce(log: HiddenErrorLog, error: unknown, request: ServerRequest<unknown> | undefined): void {
|
|
116
|
+
if (typeof error === "object" && error !== null && LOGGED.has(error)) return;
|
|
117
|
+
markLogged(error);
|
|
118
|
+
log(error, request);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** True when the policy can change a response: `"coded"`, or a `map` function. */
|
|
122
|
+
export function policyActs(policy: ServerErrorPolicy | undefined): policy is ServerErrorPolicy {
|
|
123
|
+
return policy !== undefined && (policy.expose === "coded" || policy.map !== undefined);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The call of a request, for `map`.
|
|
128
|
+
*
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
export function callOf(request: ServerRequest<unknown> | undefined): ErrorCall {
|
|
132
|
+
if (request === undefined) return { path: [] };
|
|
133
|
+
let path: ProcedurePath;
|
|
134
|
+
try {
|
|
135
|
+
path = methodToPath(request.method);
|
|
136
|
+
} catch {
|
|
137
|
+
path = [];
|
|
138
|
+
}
|
|
139
|
+
return request.transport !== undefined ? { path, transport: request.transport } : { path };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The decision of the policy for one error. */
|
|
143
|
+
type Decision =
|
|
144
|
+
| { readonly kind: "keep" }
|
|
145
|
+
| { readonly kind: "replace"; readonly error: ExposedError }
|
|
146
|
+
| { readonly kind: "hide" };
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The decision of the policy for one error. `code` is the code that the error has on the wire. A
|
|
150
|
+
* `map` result is final. `"coded"` keeps an error with a code of `codes`, or with a known code
|
|
151
|
+
* that is not a failure code.
|
|
152
|
+
*/
|
|
153
|
+
function decide(policy: ServerErrorPolicy, error: unknown, code: string, call: ErrorCall, logMapError: (error: unknown) => void): Decision {
|
|
154
|
+
if (policy.map) {
|
|
155
|
+
let mapped: ExposedError | undefined;
|
|
156
|
+
try {
|
|
157
|
+
mapped = policy.map(error, call);
|
|
158
|
+
} catch (mapError) {
|
|
159
|
+
logMapError(mapError);
|
|
160
|
+
return { kind: "hide" };
|
|
161
|
+
}
|
|
162
|
+
if (mapped !== undefined) return { kind: "replace", error: mapped };
|
|
163
|
+
}
|
|
164
|
+
if (policy.expose !== "coded") return { kind: "keep" };
|
|
165
|
+
if (policy.codes?.includes(code) === true) return { kind: "keep" };
|
|
166
|
+
if (isKnownErrorCode(code) && !FAILURE_CODES.has(code)) return { kind: "keep" };
|
|
167
|
+
return { kind: "hide" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A `map` result as an error: a result that is not an `Error` becomes one, with its fields. */
|
|
171
|
+
function errorOfResult(result: ExposedError): Error {
|
|
172
|
+
return result instanceof Error ? result : Object.assign(new Error(result.message), result);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The metadata keys that a hidden error response keeps by default: the trace keys (CLI-80). */
|
|
176
|
+
export const DEFAULT_KEPT_METADATA: readonly string[] = ["traceparent", "tracestate"];
|
|
177
|
+
|
|
178
|
+
/** The metadata of a response with only the keys of the allowlist `keepMetadata` of the policy. */
|
|
179
|
+
function keptMetadata(policy: ServerErrorPolicy, metadata: ServerResponse<unknown>["metadata"]): ServerResponse<unknown>["metadata"] {
|
|
180
|
+
const keys = policy.keepMetadata ?? DEFAULT_KEPT_METADATA;
|
|
181
|
+
const kept: Record<string, unknown> = {};
|
|
182
|
+
for (const [key, value] of Object.entries(metadata ?? {})) if (keys.includes(key)) kept[key] = value;
|
|
183
|
+
return kept as ServerResponse<unknown>["metadata"];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* This function applies the error policy to a response that leaves the server: to an error
|
|
188
|
+
* status, and to the error that ends a stream.
|
|
189
|
+
*
|
|
190
|
+
* @internal
|
|
191
|
+
*/
|
|
192
|
+
export function exposedResponse<T>(policy: ServerErrorPolicy, request: ServerRequest<unknown>, response: ServerResponse<T>, log: HiddenErrorLog): ServerResponse<T> {
|
|
193
|
+
const status = response.status;
|
|
194
|
+
if (status.type === "error") {
|
|
195
|
+
// A response that the server made without an error (a refusal of the token, a duplicate
|
|
196
|
+
// stream), or a status without the link, gives `map` a ServerError of its status
|
|
197
|
+
const link = realErrorOf(response, request);
|
|
198
|
+
const real = link.found ? link.error : new ServerError(status.message, status.code, status.retryable);
|
|
199
|
+
const decision = decide(policy, real, status.code, callOf(request), (mapError) => logOnce(log, mapError, request));
|
|
200
|
+
if (decision.kind === "keep") return response;
|
|
201
|
+
const { stream: _stream, payload: _payload, ...rest } = response;
|
|
202
|
+
// Only the keys of the allowlist stay (the trace keys): an unknown key can tell of the error
|
|
203
|
+
const kept = keptMetadata(policy, response.metadata);
|
|
204
|
+
if (decision.kind === "replace") {
|
|
205
|
+
// The usual error response of the result: its wait and its conflicts go out too
|
|
206
|
+
const built = errorResponse(response.id, errorOfResult(decision.error));
|
|
207
|
+
return { ...rest, status: built.status, metadata: { ...kept, ...built.metadata } };
|
|
208
|
+
}
|
|
209
|
+
logOnce(log, real, request);
|
|
210
|
+
return {
|
|
211
|
+
...rest,
|
|
212
|
+
// The retryable flag tells nothing private: a transient failure stays retryable
|
|
213
|
+
status: { type: "error", code: "INTERNAL_ERROR", message: HIDDEN_ERROR_MESSAGE, retryable: status.retryable },
|
|
214
|
+
metadata: kept,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (response.stream === undefined) return response;
|
|
218
|
+
return { ...response, stream: exposedStream(policy, request, response.stream, log) };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The items of a stream, with the error policy for the error that ends it. A kept error goes on
|
|
223
|
+
* unchanged. Another error goes on as an error of its exposed code and message.
|
|
224
|
+
*/
|
|
225
|
+
function exposedStream<T>(policy: ServerErrorPolicy, request: ServerRequest<unknown>, items: AsyncIterable<T>, log: HiddenErrorLog): AsyncGenerator<T, void, undefined> {
|
|
226
|
+
const exposed = (async function* () {
|
|
227
|
+
try {
|
|
228
|
+
yield* items;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
const decision = decide(policy, error, errorCodeOf(error), callOf(request), (mapError) => logOnce(log, mapError, request));
|
|
231
|
+
if (decision.kind === "keep") throw error;
|
|
232
|
+
if (decision.kind === "hide") {
|
|
233
|
+
logOnce(log, error, request);
|
|
234
|
+
// The real retryable flag of the error goes on. A last-resort catch that gets this error
|
|
235
|
+
// does not log it again.
|
|
236
|
+
const hidden = new ServerError(HIDDEN_ERROR_MESSAGE, "INTERNAL_ERROR", (error as { retryable?: unknown } | null)?.retryable === true);
|
|
237
|
+
markLogged(hidden);
|
|
238
|
+
throw hidden;
|
|
239
|
+
}
|
|
240
|
+
// Without a flag, the face gives the flag of the code (before the first item) or false
|
|
241
|
+
throw errorOfResult(decision.error);
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
return closableUnread(exposed, items);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The error that a last-resort catch sends under a policy: when `handle()` throws, or when a face
|
|
250
|
+
* fails after it. `raw` is the error that the face sends without a policy. `map` runs first. Under
|
|
251
|
+
* `"coded"`, the code of `raw` decides, so `INTERNAL_ERROR` gives `"internal error"`, and the
|
|
252
|
+
* logger gets the real error one time.
|
|
253
|
+
*/
|
|
254
|
+
export function exposeError(policy: ServerErrorPolicy | undefined, error: unknown, call: ErrorCall, log: (error: unknown) => void, raw: ExposedError = rawErrorOf(error)): ExposedError {
|
|
255
|
+
if (!policyActs(policy)) return raw;
|
|
256
|
+
const once: HiddenErrorLog = (logged) => log(logged);
|
|
257
|
+
const decision = decide(policy, error, raw.code, call, (mapError) => logOnce(once, mapError, undefined));
|
|
258
|
+
if (decision.kind === "keep") return raw;
|
|
259
|
+
if (decision.kind === "replace") return decision.error;
|
|
260
|
+
logOnce(once, error, undefined);
|
|
261
|
+
return { code: "INTERNAL_ERROR", message: HIDDEN_ERROR_MESSAGE, retryable: raw.retryable === true };
|
|
262
|
+
}
|
|
263
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error response of the server, and the link from an error status to its real error (roadmap
|
|
3
|
+
* CLI-80).
|
|
4
|
+
*
|
|
5
|
+
* The error policy of a server (`error-policy.ts`) reads the real error of a response: the error
|
|
6
|
+
* object that a handler threw, of its own class. Two links lead to it, and neither is a field of
|
|
7
|
+
* the response, so the wire, a log of the response and a deep comparison never see them:
|
|
8
|
+
* - The status object of the error response. A copy of the response (`{ ...response, id }`) keeps
|
|
9
|
+
* the same status.
|
|
10
|
+
* - The request object of the call, with the code and the message of the status. A copy of the
|
|
11
|
+
* status (`{ ...response.status }`, for example in a middleware or a wrapper) keeps the code
|
|
12
|
+
* and the message, so the link holds. A status with another code or message is another error,
|
|
13
|
+
* and has no link.
|
|
14
|
+
* - Without a link (a middleware copied both the request and the status, or made a new error),
|
|
15
|
+
* the policy gives `map` a `ServerError` of the code, the message and the retryable flag of the
|
|
16
|
+
* status.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { InvocationError } from "../procedures/invoke.js";
|
|
20
|
+
import { VERSION_CONFLICTS_METADATA_KEY, versionConflictsOf } from "../procedures/collection/precondition.js";
|
|
21
|
+
import { errorCodeOf, retryableOf } from "../protocol.js";
|
|
22
|
+
import type { ServerResponse } from "./types.js";
|
|
23
|
+
|
|
24
|
+
/** The real error of each error status that the server made from an error. */
|
|
25
|
+
const ERRORS_OF_STATUSES = new WeakMap<object, unknown>();
|
|
26
|
+
|
|
27
|
+
/** The last real error of each request, with the code and the message of its status. */
|
|
28
|
+
const ERRORS_OF_REQUESTS = new WeakMap<object, { readonly code: string; readonly message: string; readonly error: unknown }>();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* This function records the real error of an error response, for the error policy: by its status
|
|
32
|
+
* object, and by the request object of the call. The response does not change.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export function withRealError<T extends ServerResponse<unknown>>(response: T, error: unknown, request?: object): T {
|
|
37
|
+
ERRORS_OF_STATUSES.set(response.status, error);
|
|
38
|
+
const status = response.status;
|
|
39
|
+
if (request !== undefined && status.type === "error") ERRORS_OF_REQUESTS.set(request, { code: String(status.code), message: status.message, error });
|
|
40
|
+
return response;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The real error of an error status of a request, or `{ found: false }` when no link leads to it.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export function realErrorOf(response: ServerResponse<unknown>, request?: object): { readonly found: true; readonly error: unknown } | { readonly found: false } {
|
|
49
|
+
const status = response.status;
|
|
50
|
+
if (ERRORS_OF_STATUSES.has(status)) return { found: true, error: ERRORS_OF_STATUSES.get(status) };
|
|
51
|
+
const recorded = request !== undefined ? ERRORS_OF_REQUESTS.get(request) : undefined;
|
|
52
|
+
if (recorded !== undefined && status.type === "error" && recorded.code === String(status.code) && recorded.message === status.message) return { found: true, error: recorded.error };
|
|
53
|
+
return { found: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The error response of a failed invocation. An invocation error keeps its code
|
|
58
|
+
* (VALIDATION_ERROR, NOT_EXPOSED, ABORTED and so on). An error of the handler keeps its own code
|
|
59
|
+
* when it is a string, as for a stream (`WIRE-PROTOCOL.md`). Without a code, it is HANDLER_ERROR.
|
|
60
|
+
* An error without a retryable flag gets the flag of its code in `ERROR_CODES`, or false (CLI-48).
|
|
61
|
+
* An in-band end before the first item of a stream gets the same default. After the first item,
|
|
62
|
+
* an in-band end without a flag gets false (`retryableOf`, CLI-58, CLI-68, `WIRE-PROTOCOL.md`,
|
|
63
|
+
* section "Error codes"). `request` is the request of the call: the link of the error policy to the
|
|
64
|
+
* real error (CLI-80).
|
|
65
|
+
*/
|
|
66
|
+
export function errorResponse(id: string, error: unknown, request?: object): ServerResponse<unknown> {
|
|
67
|
+
// A refusal of the capability says why (CAPABILITIES-2026-10)
|
|
68
|
+
const reason = error instanceof InvocationError ? error.reason : undefined;
|
|
69
|
+
// An error can ask the client to wait before a retry: retryAfterMs, in milliseconds (R12)
|
|
70
|
+
const retryAfterMs = (error as { retryAfterMs?: unknown } | undefined)?.retryAfterMs;
|
|
71
|
+
const metadata: Record<string, unknown> = reason ? { reason } : {};
|
|
72
|
+
if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) metadata["retry-after"] = retryAfterMs;
|
|
73
|
+
// A VERSION_CONFLICT error tells the current item of each conflict (roadmap CLI-16)
|
|
74
|
+
const conflicts = versionConflictsOf(error);
|
|
75
|
+
if (conflicts !== undefined) metadata[VERSION_CONFLICTS_METADATA_KEY] = conflicts;
|
|
76
|
+
const code = errorCodeOf(error);
|
|
77
|
+
// The error policy of the server (CLI-80) reads the real error of the response
|
|
78
|
+
return withRealError({
|
|
79
|
+
id,
|
|
80
|
+
status: {
|
|
81
|
+
type: "error",
|
|
82
|
+
code,
|
|
83
|
+
message: error instanceof Error ? error.message : String(error),
|
|
84
|
+
// An error that says it is retryable stays retryable: before, every error lost the flag (deep
|
|
85
|
+
// dive TRN-7). Without a flag, the default of the code: UNAVAILABLE stays retryable, and
|
|
86
|
+
// keeps HTTP 503 (CLI-48). No item went out before this response.
|
|
87
|
+
retryable: retryableOf(error, 0),
|
|
88
|
+
},
|
|
89
|
+
metadata,
|
|
90
|
+
}, error, request);
|
|
91
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
|
|
41
41
|
export { Server } from "./server.js";
|
|
42
42
|
export type { ServerOptions } from "./server.js";
|
|
43
|
+
export type { ServerErrorPolicy, ExposedError, ErrorCall } from "./error-policy.js";
|
|
44
|
+
export { exposeError, lastResortError, HIDDEN_ERROR_MESSAGE, DEFAULT_KEPT_METADATA } from "./error-policy.js";
|
|
43
45
|
|
|
44
46
|
export { ProcedureServer, createProcedureServer, STREAM_METADATA_KEY, isUnboundedStream } from "./procedure-server.js";
|
|
45
47
|
export type { ProcedureServerOptions, RegisterProcedureOptions, StorageConfig, StopOptions, StopParts, StorageHandle, UseStorageOptions } from "./procedure-server.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The last-resort catch of a face (roadmap CLI-80): when `handle()` throws, or the face fails after
|
|
3
|
+
* it. This module has no runtime import, so a face in a browser bundle (for example
|
|
4
|
+
* `PortTransport`) does not load the error policy and its imports. The policy itself runs in the
|
|
5
|
+
* server, through `RequestHandler.exposeError`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExposedError } from "./error-policy.js";
|
|
9
|
+
import type { RequestHandler, ServerRequest } from "./types.js";
|
|
10
|
+
|
|
11
|
+
/** The error that a last-resort catch sends without a policy: `INTERNAL_ERROR` with the message of the error. */
|
|
12
|
+
export function rawErrorOf(error: unknown): ExposedError {
|
|
13
|
+
return { code: "INTERNAL_ERROR", message: error instanceof Error ? error.message || "Internal server error" : String(error), retryable: false };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The error that the last-resort catch of a face sends: the error policy of the handler
|
|
18
|
+
* (`RequestHandler.exposeError`, which `Server` has), or `raw` for a handler without one. `raw`
|
|
19
|
+
* is the error that the face sends without a policy.
|
|
20
|
+
*/
|
|
21
|
+
export function lastResortError(handler: RequestHandler, error: unknown, request: ServerRequest<unknown> | undefined, raw: ExposedError = rawErrorOf(error)): ExposedError {
|
|
22
|
+
return handler.exposeError ? handler.exposeError(error, request, raw) : raw;
|
|
23
|
+
}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Server, type ServerOptions } from "./server.js";
|
|
9
|
+
import { errorResponse } from "./error-response.js";
|
|
10
|
+
|
|
11
|
+
export { errorResponse };
|
|
9
12
|
import type { Admission, AmbientCredentials, Credentials, Method, Principal, PrincipalOf, RequestIdentity, ServerHandler, ServerRequest, ServerResponse } from "./types.js";
|
|
10
13
|
import { methodToPath, pathToMethod } from "./method.js";
|
|
11
14
|
import { isLocalCredentials } from "./request-auth.js";
|
|
@@ -21,14 +24,13 @@ import { createCollectionProcedures } from "../procedures/collection/procedures.
|
|
|
21
24
|
import { InvocationError, closableUnread, invokeProcedure, type AdmitItem, type Interceptor, type Invocation } from "../procedures/invoke.js";
|
|
22
25
|
import type { Capability, ClearanceOrder, ProcedureContext, ResourcesOf } from "../procedures/types.js";
|
|
23
26
|
import type { CollectionPartitions } from "../procedures/collection/partition.js";
|
|
24
|
-
import { VERSION_CONFLICTS_METADATA_KEY, versionConflictsOf } from "../procedures/collection/precondition.js";
|
|
25
27
|
import { TIMEOUT_METADATA_KEY, effectsOf, joinNarrowing, narrowCapability } from "../procedures/capability.js";
|
|
26
28
|
import { IdempotencyCache, idempotencyKeyOf, isRepeatable, type IdempotencyOptions } from "../procedures/idempotency.js";
|
|
27
29
|
import { optionalMs, type Duration } from "../duration.js";
|
|
28
30
|
import type { CollectionStorage } from "../procedures/collection/storage.js";
|
|
29
31
|
import { TIMER_DEFAULTS } from "../timers.js";
|
|
30
32
|
import { realClock, type Clock } from "../clock.js";
|
|
31
|
-
import {
|
|
33
|
+
import { retryableOf } from "../protocol.js";
|
|
32
34
|
import { EventPublisher, type EventPublisherOptions } from "../events/publisher.js";
|
|
33
35
|
import { getGlobalEventBus } from "../events/bus.js";
|
|
34
36
|
import type { CollectionProceduresOptions } from "../procedures/collection/procedures.js";
|
|
@@ -382,41 +384,6 @@ function withRequestTimeout(
|
|
|
382
384
|
return narrowCapability(capability, { timeoutMs }, order);
|
|
383
385
|
}
|
|
384
386
|
|
|
385
|
-
/**
|
|
386
|
-
* The error response of a failed invocation. An invocation error keeps its code
|
|
387
|
-
* (VALIDATION_ERROR, NOT_EXPOSED, ABORTED and so on). An error of the handler keeps its own code
|
|
388
|
-
* when it is a string, as for a stream (`WIRE-PROTOCOL.md`). Without a code, it is HANDLER_ERROR.
|
|
389
|
-
* An error without a retryable flag gets the flag of its code in `ERROR_CODES`, or false (CLI-48).
|
|
390
|
-
* An in-band end before the first item of a stream gets the same default. After the first item,
|
|
391
|
-
* an in-band end without a flag gets false (`retryableOf`, CLI-58, CLI-68, `WIRE-PROTOCOL.md`,
|
|
392
|
-
* section "Error codes").
|
|
393
|
-
*/
|
|
394
|
-
export function errorResponse(id: string, error: unknown): ServerResponse<unknown> {
|
|
395
|
-
// A refusal of the capability says why (CAPABILITIES-2026-10)
|
|
396
|
-
const reason = error instanceof InvocationError ? error.reason : undefined;
|
|
397
|
-
// An error can ask the client to wait before a retry: retryAfterMs, in milliseconds (R12)
|
|
398
|
-
const retryAfterMs = (error as { retryAfterMs?: unknown } | undefined)?.retryAfterMs;
|
|
399
|
-
const metadata: Record<string, unknown> = reason ? { reason } : {};
|
|
400
|
-
if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) metadata["retry-after"] = retryAfterMs;
|
|
401
|
-
// A VERSION_CONFLICT error tells the current item of each conflict (roadmap CLI-16)
|
|
402
|
-
const conflicts = versionConflictsOf(error);
|
|
403
|
-
if (conflicts !== undefined) metadata[VERSION_CONFLICTS_METADATA_KEY] = conflicts;
|
|
404
|
-
const code = errorCodeOf(error);
|
|
405
|
-
return {
|
|
406
|
-
id,
|
|
407
|
-
status: {
|
|
408
|
-
type: "error",
|
|
409
|
-
code,
|
|
410
|
-
message: error instanceof Error ? error.message : String(error),
|
|
411
|
-
// An error that says it is retryable stays retryable: before, every error lost the flag (deep
|
|
412
|
-
// dive TRN-7). Without a flag, the default of the code: UNAVAILABLE stays retryable, and
|
|
413
|
-
// keeps HTTP 503 (CLI-48). No item went out before this response.
|
|
414
|
-
retryable: retryableOf(error, 0),
|
|
415
|
-
},
|
|
416
|
-
metadata,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
387
|
// =============================================================================
|
|
421
388
|
// Procedure Server
|
|
422
389
|
// =============================================================================
|
|
@@ -977,7 +944,7 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
977
944
|
return refused ? rateLimitedResponse(request.id, refused) : undefined;
|
|
978
945
|
} catch (error) {
|
|
979
946
|
const message = `The limits of the server cannot admit the call: ${error instanceof Error ? error.message : String(error)}`;
|
|
980
|
-
return errorResponse(request.id, Object.assign(new Error(message), { code: "INTERNAL_ERROR" }));
|
|
947
|
+
return errorResponse(request.id, Object.assign(new Error(message), { code: "INTERNAL_ERROR" }), request);
|
|
981
948
|
}
|
|
982
949
|
}
|
|
983
950
|
|
|
@@ -1083,7 +1050,7 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
1083
1050
|
);
|
|
1084
1051
|
}
|
|
1085
1052
|
const response = await untilAborted(result, request.signal);
|
|
1086
|
-
if (response === undefined) return errorResponse(request.id, new InvocationError("ABORTED", "The request was aborted", procedure.path));
|
|
1053
|
+
if (response === undefined) return errorResponse(request.id, new InvocationError("ABORTED", "The request was aborted", procedure.path), request);
|
|
1087
1054
|
if (response.stream) {
|
|
1088
1055
|
return {
|
|
1089
1056
|
id: request.id,
|
|
@@ -1138,7 +1105,7 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
1138
1105
|
capability = this.capabilityOf(request);
|
|
1139
1106
|
} catch (error) {
|
|
1140
1107
|
const message = `The server cannot narrow the capability of the request: ${error instanceof Error ? error.message : String(error)}`;
|
|
1141
|
-
return errorResponse(request.id, Object.assign(new Error(message), { code: "INTERNAL_ERROR" }));
|
|
1108
|
+
return errorResponse(request.id, Object.assign(new Error(message), { code: "INTERNAL_ERROR" }), request);
|
|
1142
1109
|
}
|
|
1143
1110
|
const output = await invokeProcedure(procedure, request.payload, {
|
|
1144
1111
|
registry: this.procedureRegistry,
|
|
@@ -1175,7 +1142,7 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
1175
1142
|
}
|
|
1176
1143
|
return { id: request.id, status: { type: "success", code: 200 }, payload: output.value, metadata: {} };
|
|
1177
1144
|
} catch (error) {
|
|
1178
|
-
return errorResponse(request.id, error);
|
|
1145
|
+
return errorResponse(request.id, error, request);
|
|
1179
1146
|
} finally {
|
|
1180
1147
|
if (!streams) release();
|
|
1181
1148
|
}
|
package/src/server/server.ts
CHANGED
|
@@ -17,6 +17,8 @@ import type {
|
|
|
17
17
|
} from "./types.js";
|
|
18
18
|
import { HandlerNotFoundError, ServerError } from "./types.js";
|
|
19
19
|
import { compose } from "../middleware/index.js";
|
|
20
|
+
import { callOf, exposeError, exposedResponse, markLogged, policyActs, type ExposedError, type HiddenErrorLog, type ServerErrorPolicy } from "./error-policy.js";
|
|
21
|
+
import { withRealError } from "./error-response.js";
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Server configuration options.
|
|
@@ -30,7 +32,9 @@ export interface ServerOptions {
|
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Global error handler.
|
|
33
|
-
* Called when a handler throws an error.
|
|
35
|
+
* Called when a handler throws an error. The error policy (`errors`) also gives it each hidden
|
|
36
|
+
* error one time. A last-resort error of a face that failed before it knew the call comes with
|
|
37
|
+
* `UNKNOWN_REQUEST`: a request with the id `"unknown"`.
|
|
34
38
|
*/
|
|
35
39
|
onError?: (error: Error, request: ServerRequest) => void;
|
|
36
40
|
|
|
@@ -43,6 +47,33 @@ export interface ServerOptions {
|
|
|
43
47
|
* Response logger.
|
|
44
48
|
*/
|
|
45
49
|
onResponse?: (response: ServerResponse) => void;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The error policy (roadmap CLI-80): what an error tells the caller when it leaves the server.
|
|
53
|
+
* `handle()` applies it, so each face (HTTP, WebSocket, MCP, a message port) sends the same
|
|
54
|
+
* error. A nested call of `ctx.client` keeps the real error.
|
|
55
|
+
*
|
|
56
|
+
* - `expose: "all"` (the default): each error goes to the caller as before.
|
|
57
|
+
* - `expose: "coded"`: an error with a known code (`isKnownErrorCode`) goes to the caller as it
|
|
58
|
+
* is, and so does an error with a code of `codes`. Each other error goes as `INTERNAL_ERROR`
|
|
59
|
+
* with the message `"internal error"` and its real retryable flag. The server gives the real
|
|
60
|
+
* error to `onError` one time, or to `console.error` without `onError`.
|
|
61
|
+
* - `map` runs first, with the real error of its own class. Its result goes to the caller as it
|
|
62
|
+
* is, with its `retryAfterMs`. `undefined` gives the error to `expose`.
|
|
63
|
+
* - The last-resort catch of each face applies the same policy (`exposeError`).
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* new ProcedureServer({
|
|
68
|
+
* errors: {
|
|
69
|
+
* expose: "coded",
|
|
70
|
+
* map: (error) => (isDuplicateKey(error) ? new ServerError("The item exists", "CONFLICT") : undefined),
|
|
71
|
+
* },
|
|
72
|
+
* onError: (error, request) => logger.error({ err: error, id: request.id }),
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
errors?: ServerErrorPolicy | undefined;
|
|
46
77
|
}
|
|
47
78
|
|
|
48
79
|
/**
|
|
@@ -63,6 +94,9 @@ interface HandlerEntry {
|
|
|
63
94
|
handler: ServerHandler;
|
|
64
95
|
}
|
|
65
96
|
|
|
97
|
+
/** The request that `onError` gets for an error of a face that failed before it knew the call (CLI-80). */
|
|
98
|
+
const UNKNOWN_REQUEST: ServerRequest = { id: "unknown", method: { service: "", operation: "" }, payload: undefined, metadata: {} };
|
|
99
|
+
|
|
66
100
|
/**
|
|
67
101
|
* Universal RPC server.
|
|
68
102
|
*
|
|
@@ -196,6 +230,35 @@ export class Server {
|
|
|
196
230
|
*/
|
|
197
231
|
async handle<TReq, TRes>(
|
|
198
232
|
request: ServerRequest<TReq>
|
|
233
|
+
): Promise<ServerResponse<TRes>> {
|
|
234
|
+
const policy = this.options.errors;
|
|
235
|
+
if (!policyActs(policy)) return this.handleRaw(request);
|
|
236
|
+
const response = await this.handleRaw<TReq, TRes>(request);
|
|
237
|
+
return exposedResponse(policy, request, response, this.hiddenErrorLog);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The error that the last-resort catch of a face sends (roadmap CLI-80): when `handle()` throws,
|
|
242
|
+
* or the face fails after it. `raw` is the error that the face sends without a policy. Under the
|
|
243
|
+
* option `errors`, `map` runs first, and `"coded"` hides the message of `INTERNAL_ERROR`. The
|
|
244
|
+
* logger gets each hidden error one time. A face calls it through `lastResortError`.
|
|
245
|
+
*/
|
|
246
|
+
exposeError(error: unknown, request: ServerRequest | undefined, raw: ExposedError): ExposedError {
|
|
247
|
+
return exposeError(this.options.errors, error, callOf(request), (real) => this.hiddenErrorLog(real, request), raw);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The logger of the errors that the error policy hides: `onError`, or `console.error`. */
|
|
251
|
+
private readonly hiddenErrorLog: HiddenErrorLog = (error, request) => {
|
|
252
|
+
const real = error instanceof Error ? error : new Error(String(error));
|
|
253
|
+
// A face that failed before it knew the call gives no request: onError gets a request of no call
|
|
254
|
+
if (this.options.onError) this.options.onError(real, request ?? UNKNOWN_REQUEST);
|
|
255
|
+
// A constant format: the request id comes from the caller (CodeQL js/tainted-format-string)
|
|
256
|
+
else console.error("[Server] hidden error of %s: %o", request?.id ?? "a request", real);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/** The response of a request, before the error policy. */
|
|
260
|
+
private async handleRaw<TReq, TRes>(
|
|
261
|
+
request: ServerRequest<TReq>
|
|
199
262
|
): Promise<ServerResponse<TRes>> {
|
|
200
263
|
// Log request
|
|
201
264
|
if (this.options.onRequest) {
|
|
@@ -235,10 +298,12 @@ export class Server {
|
|
|
235
298
|
// Log error
|
|
236
299
|
if (this.options.onError) {
|
|
237
300
|
this.options.onError(error as Error, request);
|
|
301
|
+
// The error policy does not log this error again (CLI-80)
|
|
302
|
+
markLogged(error);
|
|
238
303
|
}
|
|
239
304
|
|
|
240
305
|
// Convert error to response
|
|
241
|
-
return this.errorToResponse<TRes>(request
|
|
306
|
+
return this.errorToResponse<TRes>(request, error as Error);
|
|
242
307
|
}
|
|
243
308
|
}
|
|
244
309
|
|
|
@@ -320,7 +385,12 @@ export class Server {
|
|
|
320
385
|
/**
|
|
321
386
|
* Convert error to response.
|
|
322
387
|
*/
|
|
323
|
-
private errorToResponse<TRes>(
|
|
388
|
+
private errorToResponse<TRes>(request: ServerRequest<unknown>, error: Error): ServerResponse<TRes> {
|
|
389
|
+
return withRealError(this.errorToStatus<TRes>(request.id, error), error, request);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** The response of an error, without its record for the error policy. */
|
|
393
|
+
private errorToStatus<TRes>(id: string, error: Error): ServerResponse<TRes> {
|
|
324
394
|
if (error instanceof HandlerNotFoundError) {
|
|
325
395
|
return {
|
|
326
396
|
id,
|
package/src/server/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { Elicit } from "../procedures/elicit.js";
|
|
|
9
9
|
import type { ReportProgress } from "../procedures/progress.js";
|
|
10
10
|
import type { Metadata, Status, Method } from "../client/types.js";
|
|
11
11
|
import type { CapabilityRequest } from "../procedures/types.js";
|
|
12
|
+
import type { ExposedError } from "./error-policy.js";
|
|
12
13
|
import { methodName } from "./method.js";
|
|
13
14
|
|
|
14
15
|
// Re-export Method from client types
|
|
@@ -47,6 +48,12 @@ export interface ServerRequest<TReq = unknown> {
|
|
|
47
48
|
/** Abort signal for cancellation */
|
|
48
49
|
signal?: AbortSignal;
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The face that took the request: `"http"`, `"websocket"`, `"port"` or `"mcp"` (roadmap CLI-80). The face
|
|
53
|
+
* sets it, not the caller. The `map` function of the error policy (`errors`) gets it.
|
|
54
|
+
*/
|
|
55
|
+
transport?: string | undefined;
|
|
56
|
+
|
|
50
57
|
/**
|
|
51
58
|
* The credentials of the request (roadmap R18). A network face reads them from the request: the
|
|
52
59
|
* bearer token of an `Authorization` header or of the `token` query of a WebSocket connection,
|
|
@@ -213,6 +220,12 @@ export interface ServerDebugReport {
|
|
|
213
220
|
*/
|
|
214
221
|
export interface RequestHandler {
|
|
215
222
|
handle(request: ServerRequest): Promise<ServerResponse>;
|
|
223
|
+
/**
|
|
224
|
+
* The error that the last-resort catch of a face sends, when `handle()` throws or the face fails
|
|
225
|
+
* after it (roadmap CLI-80): `raw` under the error policy of the handler. `Server` has it, and
|
|
226
|
+
* a face calls it through `lastResortError`. Absent: the face sends `raw`.
|
|
227
|
+
*/
|
|
228
|
+
exposeError?(error: unknown, request: ServerRequest | undefined, raw: ExposedError): ExposedError;
|
|
216
229
|
/**
|
|
217
230
|
* How the handler can authenticate a network request with ambient credentials: the cookies that
|
|
218
231
|
* a browser sends by itself, also for a request of another site (CSRF). A network face then
|