@webpieces/core-util 0.4.880 → 0.4.882
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/package.json +1 -1
- package/src/errors/ApiCallSite.d.ts +58 -0
- package/src/errors/ApiCallSite.js +96 -0
- package/src/errors/ApiCallSite.js.map +1 -0
- package/src/errors/ApiError.d.ts +9 -0
- package/src/errors/ApiError.js.map +1 -1
- package/src/errors/ReportableApiFailure.d.ts +43 -0
- package/src/errors/ReportableApiFailure.js +75 -0
- package/src/errors/ReportableApiFailure.js.map +1 -0
- package/src/errors/index.d.ts +3 -0
- package/src/errors/index.js +6 -1
- package/src/errors/index.js.map +1 -1
- package/src/http/WebpiecesDefaultErrorTranslator.d.ts +3 -0
- package/src/http/WebpiecesDefaultErrorTranslator.js +10 -0
- package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
- package/src/http/WebpiecesDefaultFailureClassifier.d.ts +3 -2
- package/src/http/WebpiecesDefaultFailureClassifier.js +6 -3
- package/src/http/WebpiecesDefaultFailureClassifier.js.map +1 -1
- package/src/http/api-type.js +2 -2
- package/src/http/api-type.js.map +1 -1
- package/src/index.d.ts +8 -2
- package/src/index.js +20 -1
- package/src/index.js.map +1 -1
- package/src/mcp/McpCompileAssertions.js +14 -4
- package/src/mcp/McpCompileAssertions.js.map +1 -1
- package/src/mcp/McpMetadata.d.ts +25 -13
- package/src/mcp/McpMetadata.js +31 -14
- package/src/mcp/McpMetadata.js.map +1 -1
- package/src/mcp/McpToolCatalogFile.d.ts +14 -2
- package/src/mcp/McpToolCatalogFile.js +24 -3
- package/src/mcp/McpToolCatalogFile.js.map +1 -1
- package/src/oauth/OAuthAuthorizationErrorRedirect.d.ts +37 -0
- package/src/oauth/OAuthAuthorizationErrorRedirect.js +61 -0
- package/src/oauth/OAuthAuthorizationErrorRedirect.js.map +1 -0
- package/src/oauth/OAuthBearerChallenge.d.ts +31 -0
- package/src/oauth/OAuthBearerChallenge.js +52 -0
- package/src/oauth/OAuthBearerChallenge.js.map +1 -0
- package/src/oauth/OAuthErrorCode.d.ts +69 -0
- package/src/oauth/OAuthErrorCode.js +89 -0
- package/src/oauth/OAuthErrorCode.js.map +1 -0
- package/src/oauth/OAuthErrorResponse.d.ts +34 -0
- package/src/oauth/OAuthErrorResponse.js +74 -0
- package/src/oauth/OAuthErrorResponse.js.map +1 -0
- package/src/oauth/OAuthProtocolError.d.ts +61 -0
- package/src/oauth/OAuthProtocolError.js +83 -0
- package/src/oauth/OAuthProtocolError.js.map +1 -0
- package/src/oauth/OAuthProtocolErrorCompileAssertions.d.ts +10 -0
- package/src/oauth/OAuthProtocolErrorCompileAssertions.js +39 -0
- package/src/oauth/OAuthProtocolErrorCompileAssertions.js.map +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Any function value used as the TOP of a captured stack: every frame from it upward is dropped.
|
|
3
|
+
* Its parameters are irrelevant, so `never[]` lets every generated client method be passed as-is.
|
|
4
|
+
*/
|
|
5
|
+
export type CallSiteBoundary = (...args: never[]) => unknown;
|
|
6
|
+
/**
|
|
7
|
+
* WHERE the app called a generated api client method — captured synchronously on the CALLER's
|
|
8
|
+
* stack, before the request was sent, and parked on the failure the call rejected with (#1175).
|
|
9
|
+
*
|
|
10
|
+
* WHY IT EXISTS: an api failure is CONSTRUCTED when the response arrives (`ClientErrorTranslator`,
|
|
11
|
+
* `ReceivedApiErrorRule`), long after the stack that made the call has unwound. Its own stack is
|
|
12
|
+
* therefore all framework frames — zone.js, `ProxyClient`, the translator — and not one frame of app
|
|
13
|
+
* code. Measured in a consuming app's error reporter, that meant (1) no way to see which line of app
|
|
14
|
+
* code made the failing call, and (2) every rpc failure in the app grouped into ONE issue, because
|
|
15
|
+
* every one ends in the same framework frames. One `new Error()` per call is cheap; the stack string
|
|
16
|
+
* is only formatted if somebody reads it.
|
|
17
|
+
*
|
|
18
|
+
* WHERE IT LIVES: the property `callSite` on the rejected error, NON-ENUMERABLE so it never reaches
|
|
19
|
+
* a `JSON.stringify`, a structured log record or an error reporter's "extra data" by accident. It is
|
|
20
|
+
* typed on {@link ApiError} (`err.callSite`); {@link ApiCallSite.of} reads it off any thrown value,
|
|
21
|
+
* because an app's own `ErrorTranslator` may reject with a type that is not an `ApiError`.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately NOT `cause`: error reporters follow `cause` as "this error was caused by that one",
|
|
24
|
+
* which would claim the server's 500 was caused by the click handler and bury the useful frames.
|
|
25
|
+
* Use {@link ReportableApiFailure.toReportableError} to build the report-time wrapper instead.
|
|
26
|
+
*
|
|
27
|
+
* It is an `Error` subclass only so that engines and reporters treat its `stack` as a real stack.
|
|
28
|
+
* It is never thrown.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ApiCallSite extends Error {
|
|
31
|
+
readonly apiName: string;
|
|
32
|
+
readonly methodName: string;
|
|
33
|
+
/** The property name the call site is parked under on a rejected error. */
|
|
34
|
+
static readonly PROPERTY = "callSite";
|
|
35
|
+
private constructor();
|
|
36
|
+
/** `Api.method` — the endpoint identity, for messages and grouping. */
|
|
37
|
+
get label(): string;
|
|
38
|
+
/**
|
|
39
|
+
* Capture the caller's stack NOW. Call it synchronously as the first statement of the generated
|
|
40
|
+
* client method, passing that method as `boundary`, so the top frame is the app's own line.
|
|
41
|
+
*
|
|
42
|
+
* Where the engine has no `Error.captureStackTrace` the stack keeps the client method's own frame
|
|
43
|
+
* on top of the caller's — one extra frame, never a missing one.
|
|
44
|
+
*/
|
|
45
|
+
static capture(apiName: string, methodName: string, boundary: CallSiteBoundary): ApiCallSite;
|
|
46
|
+
/**
|
|
47
|
+
* Park this call site on the value a call rejected with, WITHOUT changing that value: the same
|
|
48
|
+
* instance is rethrown, so every `instanceof` check downstream keeps working.
|
|
49
|
+
*
|
|
50
|
+
* Never clobbers: a failure that already carries a call site (an inner client call whose error
|
|
51
|
+
* propagated out through an outer one) keeps the innermost, which is the line that actually made
|
|
52
|
+
* the failing request. A non-`Error` or a frozen/sealed error is left untouched — attaching is a
|
|
53
|
+
* diagnostic nicety and must never itself throw.
|
|
54
|
+
*/
|
|
55
|
+
attachTo(failure: unknown): void;
|
|
56
|
+
/** The call site parked on `failure` by a generated client, or undefined when there is none. */
|
|
57
|
+
static of(failure: unknown): ApiCallSite | undefined;
|
|
58
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiCallSite = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* WHERE the app called a generated api client method — captured synchronously on the CALLER's
|
|
6
|
+
* stack, before the request was sent, and parked on the failure the call rejected with (#1175).
|
|
7
|
+
*
|
|
8
|
+
* WHY IT EXISTS: an api failure is CONSTRUCTED when the response arrives (`ClientErrorTranslator`,
|
|
9
|
+
* `ReceivedApiErrorRule`), long after the stack that made the call has unwound. Its own stack is
|
|
10
|
+
* therefore all framework frames — zone.js, `ProxyClient`, the translator — and not one frame of app
|
|
11
|
+
* code. Measured in a consuming app's error reporter, that meant (1) no way to see which line of app
|
|
12
|
+
* code made the failing call, and (2) every rpc failure in the app grouped into ONE issue, because
|
|
13
|
+
* every one ends in the same framework frames. One `new Error()` per call is cheap; the stack string
|
|
14
|
+
* is only formatted if somebody reads it.
|
|
15
|
+
*
|
|
16
|
+
* WHERE IT LIVES: the property `callSite` on the rejected error, NON-ENUMERABLE so it never reaches
|
|
17
|
+
* a `JSON.stringify`, a structured log record or an error reporter's "extra data" by accident. It is
|
|
18
|
+
* typed on {@link ApiError} (`err.callSite`); {@link ApiCallSite.of} reads it off any thrown value,
|
|
19
|
+
* because an app's own `ErrorTranslator` may reject with a type that is not an `ApiError`.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately NOT `cause`: error reporters follow `cause` as "this error was caused by that one",
|
|
22
|
+
* which would claim the server's 500 was caused by the click handler and bury the useful frames.
|
|
23
|
+
* Use {@link ReportableApiFailure.toReportableError} to build the report-time wrapper instead.
|
|
24
|
+
*
|
|
25
|
+
* It is an `Error` subclass only so that engines and reporters treat its `stack` as a real stack.
|
|
26
|
+
* It is never thrown.
|
|
27
|
+
*/
|
|
28
|
+
class ApiCallSite extends Error {
|
|
29
|
+
apiName;
|
|
30
|
+
methodName;
|
|
31
|
+
/** The property name the call site is parked under on a rejected error. */
|
|
32
|
+
static PROPERTY = 'callSite';
|
|
33
|
+
constructor(apiName, methodName) {
|
|
34
|
+
super(`${apiName}.${methodName} was called here`);
|
|
35
|
+
this.apiName = apiName;
|
|
36
|
+
this.methodName = methodName;
|
|
37
|
+
this.name = 'ApiCallSite';
|
|
38
|
+
}
|
|
39
|
+
/** `Api.method` — the endpoint identity, for messages and grouping. */
|
|
40
|
+
get label() {
|
|
41
|
+
return `${this.apiName}.${this.methodName}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Capture the caller's stack NOW. Call it synchronously as the first statement of the generated
|
|
45
|
+
* client method, passing that method as `boundary`, so the top frame is the app's own line.
|
|
46
|
+
*
|
|
47
|
+
* Where the engine has no `Error.captureStackTrace` the stack keeps the client method's own frame
|
|
48
|
+
* on top of the caller's — one extra frame, never a missing one.
|
|
49
|
+
*/
|
|
50
|
+
// webpieces-disable no-function-outside-class -- the factory is the only way to build one, and it must run on the caller's stack
|
|
51
|
+
static capture(apiName, methodName, boundary) {
|
|
52
|
+
const site = new ApiCallSite(apiName, methodName);
|
|
53
|
+
const capturer = Error;
|
|
54
|
+
if (typeof capturer.captureStackTrace === 'function') {
|
|
55
|
+
capturer.captureStackTrace(site, boundary);
|
|
56
|
+
}
|
|
57
|
+
return site;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Park this call site on the value a call rejected with, WITHOUT changing that value: the same
|
|
61
|
+
* instance is rethrown, so every `instanceof` check downstream keeps working.
|
|
62
|
+
*
|
|
63
|
+
* Never clobbers: a failure that already carries a call site (an inner client call whose error
|
|
64
|
+
* propagated out through an outer one) keeps the innermost, which is the line that actually made
|
|
65
|
+
* the failing request. A non-`Error` or a frozen/sealed error is left untouched — attaching is a
|
|
66
|
+
* diagnostic nicety and must never itself throw.
|
|
67
|
+
*/
|
|
68
|
+
// webpieces-disable no-any-unknown -- a rejection value is genuinely unknown; only an Error is touched
|
|
69
|
+
attachTo(failure) {
|
|
70
|
+
if (!(failure instanceof Error) || !Object.isExtensible(failure)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (Object.prototype.hasOwnProperty.call(failure, ApiCallSite.PROPERTY)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const descriptor = {
|
|
77
|
+
value: this,
|
|
78
|
+
enumerable: false,
|
|
79
|
+
writable: false,
|
|
80
|
+
configurable: true,
|
|
81
|
+
};
|
|
82
|
+
Object.defineProperty(failure, ApiCallSite.PROPERTY, descriptor);
|
|
83
|
+
}
|
|
84
|
+
/** The call site parked on `failure` by a generated client, or undefined when there is none. */
|
|
85
|
+
// webpieces-disable no-function-outside-class -- stateless reader paired with attachTo; webpieces-disable no-any-unknown -- a caught value is genuinely unknown until narrowed here
|
|
86
|
+
static of(failure) {
|
|
87
|
+
if (!(failure instanceof Error)) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
// webpieces-disable no-any-unknown -- a property descriptor's value is untyped until narrowed below
|
|
91
|
+
const value = Object.getOwnPropertyDescriptor(failure, ApiCallSite.PROPERTY)?.value;
|
|
92
|
+
return value instanceof ApiCallSite ? value : undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.ApiCallSite = ApiCallSite;
|
|
96
|
+
//# sourceMappingURL=ApiCallSite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApiCallSite.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiCallSite.ts"],"names":[],"mappings":";;;AAeA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,WAAY,SAAQ,KAAK;IAKd;IACA;IALpB,2EAA2E;IAC3E,MAAM,CAAU,QAAQ,GAAG,UAAU,CAAC;IAEtC,YACoB,OAAe,EACf,UAAkB;QAElC,KAAK,CAAC,GAAG,OAAO,IAAI,UAAU,kBAAkB,CAAC,CAAC;QAHlC,YAAO,GAAP,OAAO,CAAQ;QACf,eAAU,GAAV,UAAU,CAAQ;QAGlC,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC9B,CAAC;IAED,uEAAuE;IACvE,IAAI,KAAK;QACL,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;IAChD,CAAC;IAED;;;;;;OAMG;IACH,iIAAiI;IACjI,MAAM,CAAC,OAAO,CAAC,OAAe,EAAE,UAAkB,EAAE,QAA0B;QAC1E,MAAM,IAAI,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAClD,MAAM,QAAQ,GAAG,KAA2B,CAAC;QAC7C,IAAI,OAAO,QAAQ,CAAC,iBAAiB,KAAK,UAAU,EAAE,CAAC;YACnD,QAAQ,CAAC,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC/C,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;;;;;OAQG;IACH,uGAAuG;IACvG,QAAQ,CAAC,OAAgB;QACrB,IAAI,CAAC,CAAC,OAAO,YAAY,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/D,OAAO;QACX,CAAC;QACD,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtE,OAAO;QACX,CAAC;QACD,MAAM,UAAU,GAAuB;YACnC,KAAK,EAAE,IAAI;YACX,UAAU,EAAE,KAAK;YACjB,QAAQ,EAAE,KAAK;YACf,YAAY,EAAE,IAAI;SACrB,CAAC;QACF,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,WAAW,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IACrE,CAAC;IAED,gGAAgG;IAChG,oLAAoL;IACpL,MAAM,CAAC,EAAE,CAAC,OAAgB;QACtB,IAAI,CAAC,CAAC,OAAO,YAAY,KAAK,CAAC,EAAE,CAAC;YAC9B,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,oGAAoG;QACpG,MAAM,KAAK,GAAY,MAAM,CAAC,wBAAwB,CAClD,OAAO,EACP,WAAW,CAAC,QAAQ,CACvB,EAAE,KAAK,CAAC;QACT,OAAO,KAAK,YAAY,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5D,CAAC;;AAxEL,kCAyEC","sourcesContent":["/**\n * Any function value used as the TOP of a captured stack: every frame from it upward is dropped.\n * Its parameters are irrelevant, so `never[]` lets every generated client method be passed as-is.\n */\n// webpieces-disable no-any-unknown -- any function may be a boundary; its return value is never read\nexport type CallSiteBoundary = (...args: never[]) => unknown;\n\n/**\n * V8's (and SpiderMonkey's) non-standard stack-trimming hook. It is not in the ES `lib` typings and\n * not every engine has it, so it is read through this optional shape rather than assumed.\n */\ninterface StackTraceCapturer {\n captureStackTrace?: (target: object, boundary?: CallSiteBoundary) => void;\n}\n\n/**\n * WHERE the app called a generated api client method — captured synchronously on the CALLER's\n * stack, before the request was sent, and parked on the failure the call rejected with (#1175).\n *\n * WHY IT EXISTS: an api failure is CONSTRUCTED when the response arrives (`ClientErrorTranslator`,\n * `ReceivedApiErrorRule`), long after the stack that made the call has unwound. Its own stack is\n * therefore all framework frames — zone.js, `ProxyClient`, the translator — and not one frame of app\n * code. Measured in a consuming app's error reporter, that meant (1) no way to see which line of app\n * code made the failing call, and (2) every rpc failure in the app grouped into ONE issue, because\n * every one ends in the same framework frames. One `new Error()` per call is cheap; the stack string\n * is only formatted if somebody reads it.\n *\n * WHERE IT LIVES: the property `callSite` on the rejected error, NON-ENUMERABLE so it never reaches\n * a `JSON.stringify`, a structured log record or an error reporter's \"extra data\" by accident. It is\n * typed on {@link ApiError} (`err.callSite`); {@link ApiCallSite.of} reads it off any thrown value,\n * because an app's own `ErrorTranslator` may reject with a type that is not an `ApiError`.\n *\n * Deliberately NOT `cause`: error reporters follow `cause` as \"this error was caused by that one\",\n * which would claim the server's 500 was caused by the click handler and bury the useful frames.\n * Use {@link ReportableApiFailure.toReportableError} to build the report-time wrapper instead.\n *\n * It is an `Error` subclass only so that engines and reporters treat its `stack` as a real stack.\n * It is never thrown.\n */\nexport class ApiCallSite extends Error {\n /** The property name the call site is parked under on a rejected error. */\n static readonly PROPERTY = 'callSite';\n\n private constructor(\n public readonly apiName: string,\n public readonly methodName: string,\n ) {\n super(`${apiName}.${methodName} was called here`);\n this.name = 'ApiCallSite';\n }\n\n /** `Api.method` — the endpoint identity, for messages and grouping. */\n get label(): string {\n return `${this.apiName}.${this.methodName}`;\n }\n\n /**\n * Capture the caller's stack NOW. Call it synchronously as the first statement of the generated\n * client method, passing that method as `boundary`, so the top frame is the app's own line.\n *\n * Where the engine has no `Error.captureStackTrace` the stack keeps the client method's own frame\n * on top of the caller's — one extra frame, never a missing one.\n */\n // webpieces-disable no-function-outside-class -- the factory is the only way to build one, and it must run on the caller's stack\n static capture(apiName: string, methodName: string, boundary: CallSiteBoundary): ApiCallSite {\n const site = new ApiCallSite(apiName, methodName);\n const capturer = Error as StackTraceCapturer;\n if (typeof capturer.captureStackTrace === 'function') {\n capturer.captureStackTrace(site, boundary);\n }\n return site;\n }\n\n /**\n * Park this call site on the value a call rejected with, WITHOUT changing that value: the same\n * instance is rethrown, so every `instanceof` check downstream keeps working.\n *\n * Never clobbers: a failure that already carries a call site (an inner client call whose error\n * propagated out through an outer one) keeps the innermost, which is the line that actually made\n * the failing request. A non-`Error` or a frozen/sealed error is left untouched — attaching is a\n * diagnostic nicety and must never itself throw.\n */\n // webpieces-disable no-any-unknown -- a rejection value is genuinely unknown; only an Error is touched\n attachTo(failure: unknown): void {\n if (!(failure instanceof Error) || !Object.isExtensible(failure)) {\n return;\n }\n if (Object.prototype.hasOwnProperty.call(failure, ApiCallSite.PROPERTY)) {\n return;\n }\n const descriptor: PropertyDescriptor = {\n value: this,\n enumerable: false,\n writable: false,\n configurable: true,\n };\n Object.defineProperty(failure, ApiCallSite.PROPERTY, descriptor);\n }\n\n /** The call site parked on `failure` by a generated client, or undefined when there is none. */\n // webpieces-disable no-function-outside-class -- stateless reader paired with attachTo; webpieces-disable no-any-unknown -- a caught value is genuinely unknown until narrowed here\n static of(failure: unknown): ApiCallSite | undefined {\n if (!(failure instanceof Error)) {\n return undefined;\n }\n // webpieces-disable no-any-unknown -- a property descriptor's value is untyped until narrowed below\n const value: unknown = Object.getOwnPropertyDescriptor(\n failure,\n ApiCallSite.PROPERTY,\n )?.value;\n return value instanceof ApiCallSite ? value : undefined;\n }\n}\n"]}
|
package/src/errors/ApiError.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import { ApiCallSite } from './ApiCallSite';
|
|
1
2
|
export type ApiErrorKind = 'end-user' | 'bad-request' | 'unauthorized' | 'forbidden' | 'not-found' | 'endpoint-not-found' | 'request-timeout' | 'rate-limited' | 'conflict' | 'unprocessable' | 'precondition-failed' | 'unsupported-media-type' | 'not-implemented' | 'coded' | 'implementation' | 'dependency' | 'bad-gateway' | 'unavailable' | 'dependency-timeout' | 'dependency-backoff' | 'connection';
|
|
2
3
|
/** Category for portable API failures. Concrete subclasses alone define protocol mappings. */
|
|
3
4
|
export declare abstract class ApiError extends Error {
|
|
4
5
|
abstract readonly kind: ApiErrorKind;
|
|
5
6
|
subType?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Where the app called the generated client method that rejected with this error — present only
|
|
9
|
+
* on an error a generated api client (HTTP, IPC or Cloud Tasks) rejected with. Set by
|
|
10
|
+
* {@link ApiCallSite.attachTo} as a NON-ENUMERABLE own property, so it never reaches JSON or a
|
|
11
|
+
* structured log; `declare` emits no field, so an error that never failed a client call carries
|
|
12
|
+
* no `callSite` key at all. See {@link ApiCallSite} for why it is not `cause`.
|
|
13
|
+
*/
|
|
14
|
+
readonly callSite?: ApiCallSite;
|
|
6
15
|
constructor(message: string, cause?: Error);
|
|
7
16
|
}
|
|
8
17
|
export declare class ApiNotFoundError extends ApiError {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiError.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiError.ts"],"names":[],"mappings":"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","sourcesContent":["export type ApiErrorKind =\n | 'end-user'\n | 'bad-request'\n | 'unauthorized'\n | 'forbidden'\n | 'not-found'\n | 'endpoint-not-found'\n | 'request-timeout'\n | 'rate-limited'\n | 'conflict'\n | 'unprocessable'\n | 'precondition-failed'\n | 'unsupported-media-type'\n | 'not-implemented'\n | 'coded'\n | 'implementation'\n | 'dependency'\n | 'bad-gateway'\n | 'unavailable'\n | 'dependency-timeout'\n | 'dependency-backoff'\n | 'connection';\n\n/** Category for portable API failures. Concrete subclasses alone define protocol mappings. */\nexport abstract class ApiError extends Error {\n abstract readonly kind: ApiErrorKind;\n public subType?: string;\n\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = new.target.name;\n }\n}\n\nexport class ApiNotFoundError extends ApiError {\n override readonly kind: ApiErrorKind = 'not-found';\n}\nexport class ApiDependencyError extends ApiError {\n override readonly kind: ApiErrorKind = 'dependency';\n}\n/** A retry-eligible HTTP 502/proxy failure, distinct from a generic dependency defect. */\nexport class ApiBadGatewayError extends ApiDependencyError {\n override readonly kind = 'bad-gateway' as const;\n}\nexport class ApiUnavailableError extends ApiDependencyError {\n override readonly kind = 'unavailable' as const;\n}\nexport class ApiDependencyTimeoutError extends ApiDependencyError {\n override readonly kind = 'dependency-timeout' as const;\n}\n\n/** A bug or invalid implementation state. Local errors become server errors only after a remote decode. */\nexport class ApiImplementationError extends ApiError {\n override readonly kind = 'implementation' as const;\n\n constructor(\n message: string,\n cause?: Error,\n public readonly serverError = false,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiRateLimitedError extends ApiError {\n override readonly kind = 'rate-limited' as const;\n}\nexport class ApiForbiddenError extends ApiError {\n override readonly kind = 'forbidden' as const;\n}\nexport class ApiRequestTimeoutError extends ApiError {\n override readonly kind = 'request-timeout' as const;\n}\n\n/**\n * A caller-local connection failure. HTTP servers normalize this to ApiImplementationError.\n *\n * `message` is the developer text (it names the url and the transport's own reason); `userMessage`\n * is the stable text an app renders verbatim to an end user, matching `ApiCallTimeoutError.userMessage`\n * so the two transport failures read consistently.\n */\nexport class ApiConnectionError extends ApiError {\n static readonly USER_MESSAGE =\n \"We couldn't reach the server. Please check your network connection and try again.\";\n\n override readonly kind = 'connection' as const;\n /** End-user text; `message` stays the developer text. */\n readonly userMessage: string = ApiConnectionError.USER_MESSAGE;\n}\n\n/** The API operation is missing, distinct from an absent domain entity. */\nexport class ApiEndpointNotFoundError extends ApiNotFoundError {\n override readonly kind = 'endpoint-not-found' as const;\n}\n\n/**\n * The HTTP status an API EDGE answers for an {@link ApiEndUserError}. End-user outcomes only, so\n * nobody tunnels a 5xx/401/403/429 through the end-user channel (auth, rate limiting and dependency\n * failure keep their own types). Extend deliberately.\n */\nexport type EdgeHttpStatus = 400 | 404 | 409 | 422;\n\n/**\n * Expected mistake by the ACTOR driving the call: the input was well-formed but wrong in a way the actor\n * must fix (for example \"the two passwords you entered do not match\"). The actor may be a human user in\n * a GUI OR an LLM calling an MCP tool; the same class serves both. Its message is deliberately\n * caller-safe and is the only ApiError message published verbatim: `ApiErrorCodec` keeps it (and\n * `errorCode`) across every remote hop, so a downstream service's text reaches the GUI or the model\n * byte-for-byte.\n *\n * `edgeHttpStatus` travels the same way. It is what a partner-facing API edge (an http-server router\n * in `'edge'` end-user-status mode) answers instead of 266, so one throw site serves both a GUI and a\n * published REST contract: `throw new ApiEndUserError('That report does not exist', 'report_not_found', 404)`.\n * GUI edges ignore it and keep answering 266.\n */\nexport class ApiEndUserError extends ApiError {\n override readonly kind = 'end-user' as const;\n\n constructor(\n message: string,\n public errorCode?: string,\n public edgeHttpStatus?: EdgeHttpStatus,\n cause?: Error,\n ) {\n super(message, cause);\n this.subType = 'USER_ERROR';\n }\n\n /** Narrows a dynamic value (for example one read off the wire) to a legal {@link EdgeHttpStatus}. */\n // webpieces-disable no-function-outside-class -- stateless status guard; webpieces-disable no-any-unknown -- wire values are narrowed here\n static isEdgeHttpStatus(value: unknown): value is EdgeHttpStatus {\n return value === 400 || value === 404 || value === 409 || value === 422;\n }\n}\n\n/**\n * Invalid API caller input (malformed, or failed validation). `message` is operator-only diagnostic\n * text and may name internals; it is never published. `callerMessage` (plus `field`) is what a GUI OR\n * an MCP model is shown so it can correct its input, so throw sites that want the caller to\n * self-correct MUST set it.\n */\nexport class ApiBadRequestError extends ApiError {\n override readonly kind = 'bad-request' as const;\n\n constructor(\n message: string,\n public field?: string,\n public callerMessage?: string,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiUnauthorizedError extends ApiError {\n override readonly kind = 'unauthorized' as const;\n\n constructor(message: string, subType?: string, cause?: Error) {\n super(message, cause);\n this.subType = subType;\n }\n}\n\nexport class ApiDependencyBackoffError extends ApiDependencyError {\n override readonly kind = 'dependency-backoff' as const;\n\n constructor(\n message: string,\n public retryAfterSeconds = 30,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n\n/** The request conflicts with the current state of the target resource (HTTP 409). */\nexport class ApiConflictError extends ApiError {\n override readonly kind = 'conflict' as const;\n}\n\n/** The request was well-formed but semantically unprocessable (HTTP 422). */\nexport class ApiUnprocessableError extends ApiError {\n override readonly kind = 'unprocessable' as const;\n}\n\n/** A caller-supplied precondition (for example If-Match) did not hold (HTTP 412). */\nexport class ApiPreconditionFailedError extends ApiError {\n override readonly kind = 'precondition-failed' as const;\n}\n\n/** The request body is in a media type this operation does not accept (HTTP 415). */\nexport class ApiUnsupportedMediaTypeError extends ApiError {\n override readonly kind = 'unsupported-media-type' as const;\n}\n\n/** The operation exists in the contract but this server does not implement it (HTTP 501). */\nexport class ApiNotImplementedError extends ApiError {\n override readonly kind = 'not-implemented' as const;\n}\n\n/** Builds the tuple [0, 1, ..., N-1]; tail-recursive so TypeScript can reach 600. */\ntype StatusRange<N extends number, Acc extends number[] = []> = Acc['length'] extends N\n ? Acc\n : StatusRange<N, [...Acc, Acc['length']]>;\n\n/**\n * Every integer from 100 through 599 as a literal union. `new ApiCodedError(msg, 600)`,\n * `new ApiCodedError(msg, 99)` and `new ApiCodedError(msg, 404.5)` do not compile; a dynamic\n * `number` must be narrowed with {@link ApiCodedError.isStatusCode} first.\n */\nexport type ApiStatusCode = Exclude<StatusRange<600>[number], StatusRange<100>[number]>;\n\n/**\n * Catch-all carrying an explicit protocol status for outcomes no named ApiError covers. Any code in\n * 100-599 is accepted, including one a named class already owns. `statusCode` and `errorCode` survive\n * every remote hop through `ApiErrorCodec`; `message` stays operator-only like every other non\n * end-user kind. A status below 500 is a caller outcome (except 408 and 429, which mirror\n * `ApiRequestTimeoutError` and `ApiRateLimitedError`); 500 and above is a server/dependency fault.\n */\nexport class ApiCodedError extends ApiError {\n override readonly kind = 'coded' as const;\n\n constructor(\n message: string,\n public readonly statusCode: ApiStatusCode,\n public errorCode?: string,\n cause?: Error,\n ) {\n super(message, cause);\n }\n\n /** Narrows a dynamic number (for example one read off the wire) to a legal status code. */\n // webpieces-disable no-function-outside-class -- stateless status guard; webpieces-disable no-any-unknown -- wire and app values are narrowed here\n static isStatusCode(value: unknown): value is ApiStatusCode {\n return typeof value === 'number' && Number.isInteger(value) && value >= 100 && value <= 599;\n }\n\n /** True when the status describes the caller's mistake rather than a fault on the serving side. */\n isCallerError(): boolean {\n return this.statusCode < 500 && this.statusCode !== 408 && this.statusCode !== 429;\n }\n}\n\n/**\n * The CALLER's build is too old for this server: HTTP **426 Upgrade Required**, errorCode\n * {@link ApiClientTooOldError.CODE}. It means \"upgrade\", never \"bug\".\n *\n * WHO THROWS IT: a server gate that compares the build the caller DECLARED (a header, a version in\n * the request) against the minimum this server still supports. Every webpieces client and server\n * deployed separately hits this skew (a browser tab left open across a release, a mobile WebView\n * pinned to an old bundle), and only the SERVER can tell that its caller is too old.\n *\n * WHY 426 AND NOT 266: a 266 means \"the user must fix something\" and its message is shown verbatim,\n * so a bundle built before the gate existed would print the server's developer text as a user\n * instruction. A non-266 status reaches every bundle already in the field as a non-user failure.\n *\n * WHAT RECEIVERS DO: `ReceivedApiErrorRule` decodes it back into THIS type on every hop\n * (server-to-server and browser, HTTP and IPC) instead of the `ApiImplementationError` an ordinary\n * received 4xx becomes. A client treats it as \"reload / install the new build\", never as a red bug\n * dialog. It pairs with `allowUpgradeInFlight` (`HttpEndpointOptions`), which declares which calls\n * an upgrade may cut off: that flag is the client deciding WHEN it may upgrade, this error is the\n * server saying that it MUST.\n *\n * NO user-facing copy: `message` is developer text like every non-end-user kind (it is not published\n * across the wire), and the receiving client owns the wording it shows. The status and the code are\n * FIXED here, so a throw site cannot send a 426 that the receivers would not recognise.\n *\n * It is an {@link ApiCodedError} on purpose: the wire form is exactly `kind: 'coded'`,\n * `statusCode: 426`, `errorCode: 'client-too-old'`, which is what an app's own hand-rolled\n * client-too-old error already publishes, so a bundle in the field and a server on the new release\n * agree without either side changing its payload.\n */\nexport class ApiClientTooOldError extends ApiCodedError {\n /** The fixed `errorCode`; together with status 426 it is what a receiver matches on. */\n static readonly CODE = 'client-too-old';\n /** HTTP 426 Upgrade Required. */\n static readonly STATUS_CODE = 426;\n\n constructor(message: string, cause?: Error) {\n super(message, ApiClientTooOldError.STATUS_CODE, ApiClientTooOldError.CODE, cause);\n }\n\n /** True when a coded failure's status and code are this error's wire identity. */\n // webpieces-disable no-function-outside-class -- stateless wire-identity predicate\n static matches(statusCode: number, errorCode: string | undefined): boolean {\n return (\n statusCode === ApiClientTooOldError.STATUS_CODE &&\n errorCode === ApiClientTooOldError.CODE\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ApiError.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiError.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiCallSite } from './ApiCallSite';\n\nexport type ApiErrorKind =\n | 'end-user'\n | 'bad-request'\n | 'unauthorized'\n | 'forbidden'\n | 'not-found'\n | 'endpoint-not-found'\n | 'request-timeout'\n | 'rate-limited'\n | 'conflict'\n | 'unprocessable'\n | 'precondition-failed'\n | 'unsupported-media-type'\n | 'not-implemented'\n | 'coded'\n | 'implementation'\n | 'dependency'\n | 'bad-gateway'\n | 'unavailable'\n | 'dependency-timeout'\n | 'dependency-backoff'\n | 'connection';\n\n/** Category for portable API failures. Concrete subclasses alone define protocol mappings. */\nexport abstract class ApiError extends Error {\n abstract readonly kind: ApiErrorKind;\n public subType?: string;\n\n /**\n * Where the app called the generated client method that rejected with this error — present only\n * on an error a generated api client (HTTP, IPC or Cloud Tasks) rejected with. Set by\n * {@link ApiCallSite.attachTo} as a NON-ENUMERABLE own property, so it never reaches JSON or a\n * structured log; `declare` emits no field, so an error that never failed a client call carries\n * no `callSite` key at all. See {@link ApiCallSite} for why it is not `cause`.\n */\n declare readonly callSite?: ApiCallSite;\n\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = new.target.name;\n }\n}\n\nexport class ApiNotFoundError extends ApiError {\n override readonly kind: ApiErrorKind = 'not-found';\n}\nexport class ApiDependencyError extends ApiError {\n override readonly kind: ApiErrorKind = 'dependency';\n}\n/** A retry-eligible HTTP 502/proxy failure, distinct from a generic dependency defect. */\nexport class ApiBadGatewayError extends ApiDependencyError {\n override readonly kind = 'bad-gateway' as const;\n}\nexport class ApiUnavailableError extends ApiDependencyError {\n override readonly kind = 'unavailable' as const;\n}\nexport class ApiDependencyTimeoutError extends ApiDependencyError {\n override readonly kind = 'dependency-timeout' as const;\n}\n\n/** A bug or invalid implementation state. Local errors become server errors only after a remote decode. */\nexport class ApiImplementationError extends ApiError {\n override readonly kind = 'implementation' as const;\n\n constructor(\n message: string,\n cause?: Error,\n public readonly serverError = false,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiRateLimitedError extends ApiError {\n override readonly kind = 'rate-limited' as const;\n}\nexport class ApiForbiddenError extends ApiError {\n override readonly kind = 'forbidden' as const;\n}\nexport class ApiRequestTimeoutError extends ApiError {\n override readonly kind = 'request-timeout' as const;\n}\n\n/**\n * A caller-local connection failure. HTTP servers normalize this to ApiImplementationError.\n *\n * `message` is the developer text (it names the url and the transport's own reason); `userMessage`\n * is the stable text an app renders verbatim to an end user, matching `ApiCallTimeoutError.userMessage`\n * so the two transport failures read consistently.\n */\nexport class ApiConnectionError extends ApiError {\n static readonly USER_MESSAGE =\n \"We couldn't reach the server. Please check your network connection and try again.\";\n\n override readonly kind = 'connection' as const;\n /** End-user text; `message` stays the developer text. */\n readonly userMessage: string = ApiConnectionError.USER_MESSAGE;\n}\n\n/** The API operation is missing, distinct from an absent domain entity. */\nexport class ApiEndpointNotFoundError extends ApiNotFoundError {\n override readonly kind = 'endpoint-not-found' as const;\n}\n\n/**\n * The HTTP status an API EDGE answers for an {@link ApiEndUserError}. End-user outcomes only, so\n * nobody tunnels a 5xx/401/403/429 through the end-user channel (auth, rate limiting and dependency\n * failure keep their own types). Extend deliberately.\n */\nexport type EdgeHttpStatus = 400 | 404 | 409 | 422;\n\n/**\n * Expected mistake by the ACTOR driving the call: the input was well-formed but wrong in a way the actor\n * must fix (for example \"the two passwords you entered do not match\"). The actor may be a human user in\n * a GUI OR an LLM calling an MCP tool; the same class serves both. Its message is deliberately\n * caller-safe and is the only ApiError message published verbatim: `ApiErrorCodec` keeps it (and\n * `errorCode`) across every remote hop, so a downstream service's text reaches the GUI or the model\n * byte-for-byte.\n *\n * `edgeHttpStatus` travels the same way. It is what a partner-facing API edge (an http-server router\n * in `'edge'` end-user-status mode) answers instead of 266, so one throw site serves both a GUI and a\n * published REST contract: `throw new ApiEndUserError('That report does not exist', 'report_not_found', 404)`.\n * GUI edges ignore it and keep answering 266.\n */\nexport class ApiEndUserError extends ApiError {\n override readonly kind = 'end-user' as const;\n\n constructor(\n message: string,\n public errorCode?: string,\n public edgeHttpStatus?: EdgeHttpStatus,\n cause?: Error,\n ) {\n super(message, cause);\n this.subType = 'USER_ERROR';\n }\n\n /** Narrows a dynamic value (for example one read off the wire) to a legal {@link EdgeHttpStatus}. */\n // webpieces-disable no-function-outside-class -- stateless status guard; webpieces-disable no-any-unknown -- wire values are narrowed here\n static isEdgeHttpStatus(value: unknown): value is EdgeHttpStatus {\n return value === 400 || value === 404 || value === 409 || value === 422;\n }\n}\n\n/**\n * Invalid API caller input (malformed, or failed validation). `message` is operator-only diagnostic\n * text and may name internals; it is never published. `callerMessage` (plus `field`) is what a GUI OR\n * an MCP model is shown so it can correct its input, so throw sites that want the caller to\n * self-correct MUST set it.\n */\nexport class ApiBadRequestError extends ApiError {\n override readonly kind = 'bad-request' as const;\n\n constructor(\n message: string,\n public field?: string,\n public callerMessage?: string,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiUnauthorizedError extends ApiError {\n override readonly kind = 'unauthorized' as const;\n\n constructor(message: string, subType?: string, cause?: Error) {\n super(message, cause);\n this.subType = subType;\n }\n}\n\nexport class ApiDependencyBackoffError extends ApiDependencyError {\n override readonly kind = 'dependency-backoff' as const;\n\n constructor(\n message: string,\n public retryAfterSeconds = 30,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n\n/** The request conflicts with the current state of the target resource (HTTP 409). */\nexport class ApiConflictError extends ApiError {\n override readonly kind = 'conflict' as const;\n}\n\n/** The request was well-formed but semantically unprocessable (HTTP 422). */\nexport class ApiUnprocessableError extends ApiError {\n override readonly kind = 'unprocessable' as const;\n}\n\n/** A caller-supplied precondition (for example If-Match) did not hold (HTTP 412). */\nexport class ApiPreconditionFailedError extends ApiError {\n override readonly kind = 'precondition-failed' as const;\n}\n\n/** The request body is in a media type this operation does not accept (HTTP 415). */\nexport class ApiUnsupportedMediaTypeError extends ApiError {\n override readonly kind = 'unsupported-media-type' as const;\n}\n\n/** The operation exists in the contract but this server does not implement it (HTTP 501). */\nexport class ApiNotImplementedError extends ApiError {\n override readonly kind = 'not-implemented' as const;\n}\n\n/** Builds the tuple [0, 1, ..., N-1]; tail-recursive so TypeScript can reach 600. */\ntype StatusRange<N extends number, Acc extends number[] = []> = Acc['length'] extends N\n ? Acc\n : StatusRange<N, [...Acc, Acc['length']]>;\n\n/**\n * Every integer from 100 through 599 as a literal union. `new ApiCodedError(msg, 600)`,\n * `new ApiCodedError(msg, 99)` and `new ApiCodedError(msg, 404.5)` do not compile; a dynamic\n * `number` must be narrowed with {@link ApiCodedError.isStatusCode} first.\n */\nexport type ApiStatusCode = Exclude<StatusRange<600>[number], StatusRange<100>[number]>;\n\n/**\n * Catch-all carrying an explicit protocol status for outcomes no named ApiError covers. Any code in\n * 100-599 is accepted, including one a named class already owns. `statusCode` and `errorCode` survive\n * every remote hop through `ApiErrorCodec`; `message` stays operator-only like every other non\n * end-user kind. A status below 500 is a caller outcome (except 408 and 429, which mirror\n * `ApiRequestTimeoutError` and `ApiRateLimitedError`); 500 and above is a server/dependency fault.\n */\nexport class ApiCodedError extends ApiError {\n override readonly kind = 'coded' as const;\n\n constructor(\n message: string,\n public readonly statusCode: ApiStatusCode,\n public errorCode?: string,\n cause?: Error,\n ) {\n super(message, cause);\n }\n\n /** Narrows a dynamic number (for example one read off the wire) to a legal status code. */\n // webpieces-disable no-function-outside-class -- stateless status guard; webpieces-disable no-any-unknown -- wire and app values are narrowed here\n static isStatusCode(value: unknown): value is ApiStatusCode {\n return typeof value === 'number' && Number.isInteger(value) && value >= 100 && value <= 599;\n }\n\n /** True when the status describes the caller's mistake rather than a fault on the serving side. */\n isCallerError(): boolean {\n return this.statusCode < 500 && this.statusCode !== 408 && this.statusCode !== 429;\n }\n}\n\n/**\n * The CALLER's build is too old for this server: HTTP **426 Upgrade Required**, errorCode\n * {@link ApiClientTooOldError.CODE}. It means \"upgrade\", never \"bug\".\n *\n * WHO THROWS IT: a server gate that compares the build the caller DECLARED (a header, a version in\n * the request) against the minimum this server still supports. Every webpieces client and server\n * deployed separately hits this skew (a browser tab left open across a release, a mobile WebView\n * pinned to an old bundle), and only the SERVER can tell that its caller is too old.\n *\n * WHY 426 AND NOT 266: a 266 means \"the user must fix something\" and its message is shown verbatim,\n * so a bundle built before the gate existed would print the server's developer text as a user\n * instruction. A non-266 status reaches every bundle already in the field as a non-user failure.\n *\n * WHAT RECEIVERS DO: `ReceivedApiErrorRule` decodes it back into THIS type on every hop\n * (server-to-server and browser, HTTP and IPC) instead of the `ApiImplementationError` an ordinary\n * received 4xx becomes. A client treats it as \"reload / install the new build\", never as a red bug\n * dialog. It pairs with `allowUpgradeInFlight` (`HttpEndpointOptions`), which declares which calls\n * an upgrade may cut off: that flag is the client deciding WHEN it may upgrade, this error is the\n * server saying that it MUST.\n *\n * NO user-facing copy: `message` is developer text like every non-end-user kind (it is not published\n * across the wire), and the receiving client owns the wording it shows. The status and the code are\n * FIXED here, so a throw site cannot send a 426 that the receivers would not recognise.\n *\n * It is an {@link ApiCodedError} on purpose: the wire form is exactly `kind: 'coded'`,\n * `statusCode: 426`, `errorCode: 'client-too-old'`, which is what an app's own hand-rolled\n * client-too-old error already publishes, so a bundle in the field and a server on the new release\n * agree without either side changing its payload.\n */\nexport class ApiClientTooOldError extends ApiCodedError {\n /** The fixed `errorCode`; together with status 426 it is what a receiver matches on. */\n static readonly CODE = 'client-too-old';\n /** HTTP 426 Upgrade Required. */\n static readonly STATUS_CODE = 426;\n\n constructor(message: string, cause?: Error) {\n super(message, ApiClientTooOldError.STATUS_CODE, ApiClientTooOldError.CODE, cause);\n }\n\n /** True when a coded failure's status and code are this error's wire identity. */\n // webpieces-disable no-function-outside-class -- stateless wire-identity predicate\n static matches(statusCode: number, errorCode: string | undefined): boolean {\n return (\n statusCode === ApiClientTooOldError.STATUS_CODE &&\n errorCode === ApiClientTooOldError.CODE\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ApiCallSite } from './ApiCallSite';
|
|
2
|
+
/**
|
|
3
|
+
* The report-time wrapper for a failed api call: named `RpcCallFailed`, message
|
|
4
|
+
* `Api.method failed: <original message>`, the CALL SITE's stack, and `cause` = the original error.
|
|
5
|
+
*
|
|
6
|
+
* NEVER THROWN. It exists only to be handed to an error reporter, so that the reported stack is the
|
|
7
|
+
* app line that made the call while the original failure (and its own framework stack) is still
|
|
8
|
+
* one `cause` hop away. Here `cause` is honest — the call failed BECAUSE of the original error —
|
|
9
|
+
* which is exactly why the call site itself is not stored as the original's `cause`.
|
|
10
|
+
*/
|
|
11
|
+
export declare class RpcCallFailed extends Error {
|
|
12
|
+
readonly callSite: ApiCallSite;
|
|
13
|
+
constructor(callSite: ApiCallSite, original: Error);
|
|
14
|
+
/**
|
|
15
|
+
* The call site's frames without its own header line. V8 prefixes a stack with
|
|
16
|
+
* `Name: message`; SpiderMonkey and JavaScriptCore do not, so only a matching first line is cut.
|
|
17
|
+
*/
|
|
18
|
+
private static framesOf;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What an error reporter needs to report a failed api call well: the {@link RpcCallFailed} wrapper
|
|
22
|
+
* and a suggested grouping `fingerprint` of `[original.name, 'Api.method']`, so failures group per
|
|
23
|
+
* endpoint and error type instead of collapsing into one issue for the whole app.
|
|
24
|
+
*
|
|
25
|
+
* Reporter-agnostic on purpose — webpieces does not depend on any error-reporting SDK. A Sentry app,
|
|
26
|
+
* for example, would do:
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* const report = ReportableApiFailure.toReportableError(err);
|
|
30
|
+
* if (report) Sentry.captureException(report.error, { fingerprint: [...report.fingerprint] });
|
|
31
|
+
* else Sentry.captureException(err);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare class ReportableApiFailure {
|
|
35
|
+
readonly error: RpcCallFailed;
|
|
36
|
+
readonly fingerprint: readonly string[];
|
|
37
|
+
constructor(error: RpcCallFailed, fingerprint: readonly string[]);
|
|
38
|
+
/**
|
|
39
|
+
* The reportable form of `failure`, or undefined when it carries no call site (it did not come
|
|
40
|
+
* out of a generated api client, so there is nothing to add — report it as it is).
|
|
41
|
+
*/
|
|
42
|
+
static toReportableError(failure: unknown): ReportableApiFailure | undefined;
|
|
43
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReportableApiFailure = exports.RpcCallFailed = void 0;
|
|
4
|
+
const ApiCallSite_1 = require("./ApiCallSite");
|
|
5
|
+
/**
|
|
6
|
+
* The report-time wrapper for a failed api call: named `RpcCallFailed`, message
|
|
7
|
+
* `Api.method failed: <original message>`, the CALL SITE's stack, and `cause` = the original error.
|
|
8
|
+
*
|
|
9
|
+
* NEVER THROWN. It exists only to be handed to an error reporter, so that the reported stack is the
|
|
10
|
+
* app line that made the call while the original failure (and its own framework stack) is still
|
|
11
|
+
* one `cause` hop away. Here `cause` is honest — the call failed BECAUSE of the original error —
|
|
12
|
+
* which is exactly why the call site itself is not stored as the original's `cause`.
|
|
13
|
+
*/
|
|
14
|
+
class RpcCallFailed extends Error {
|
|
15
|
+
callSite;
|
|
16
|
+
constructor(callSite, original) {
|
|
17
|
+
super(`${callSite.label} failed: ${original.message}`, { cause: original });
|
|
18
|
+
this.callSite = callSite;
|
|
19
|
+
this.name = 'RpcCallFailed';
|
|
20
|
+
this.stack = `${this.name}: ${this.message}\n${RpcCallFailed.framesOf(callSite)}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The call site's frames without its own header line. V8 prefixes a stack with
|
|
24
|
+
* `Name: message`; SpiderMonkey and JavaScriptCore do not, so only a matching first line is cut.
|
|
25
|
+
*/
|
|
26
|
+
// webpieces-disable no-function-outside-class -- pure stack-text helper owned by this class
|
|
27
|
+
static framesOf(callSite) {
|
|
28
|
+
const stack = callSite.stack ?? '';
|
|
29
|
+
const header = `${callSite.name}: ${callSite.message}`;
|
|
30
|
+
if (!stack.startsWith(header)) {
|
|
31
|
+
return stack;
|
|
32
|
+
}
|
|
33
|
+
return stack.slice(header.length).replace(/^\n/, '');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.RpcCallFailed = RpcCallFailed;
|
|
37
|
+
/**
|
|
38
|
+
* What an error reporter needs to report a failed api call well: the {@link RpcCallFailed} wrapper
|
|
39
|
+
* and a suggested grouping `fingerprint` of `[original.name, 'Api.method']`, so failures group per
|
|
40
|
+
* endpoint and error type instead of collapsing into one issue for the whole app.
|
|
41
|
+
*
|
|
42
|
+
* Reporter-agnostic on purpose — webpieces does not depend on any error-reporting SDK. A Sentry app,
|
|
43
|
+
* for example, would do:
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* const report = ReportableApiFailure.toReportableError(err);
|
|
47
|
+
* if (report) Sentry.captureException(report.error, { fingerprint: [...report.fingerprint] });
|
|
48
|
+
* else Sentry.captureException(err);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
class ReportableApiFailure {
|
|
52
|
+
error;
|
|
53
|
+
fingerprint;
|
|
54
|
+
constructor(error, fingerprint) {
|
|
55
|
+
this.error = error;
|
|
56
|
+
this.fingerprint = fingerprint;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The reportable form of `failure`, or undefined when it carries no call site (it did not come
|
|
60
|
+
* out of a generated api client, so there is nothing to add — report it as it is).
|
|
61
|
+
*/
|
|
62
|
+
// webpieces-disable no-function-outside-class -- stateless factory over a caught value; webpieces-disable no-any-unknown -- a caught value is genuinely unknown until narrowed here
|
|
63
|
+
static toReportableError(failure) {
|
|
64
|
+
const callSite = ApiCallSite_1.ApiCallSite.of(failure);
|
|
65
|
+
if (callSite === undefined || !(failure instanceof Error)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return new ReportableApiFailure(new RpcCallFailed(callSite, failure), [
|
|
69
|
+
failure.name,
|
|
70
|
+
callSite.label,
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.ReportableApiFailure = ReportableApiFailure;
|
|
75
|
+
//# sourceMappingURL=ReportableApiFailure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReportableApiFailure.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ReportableApiFailure.ts"],"names":[],"mappings":";;;AAAA,+CAA4C;AAE5C;;;;;;;;GAQG;AACH,MAAa,aAAc,SAAQ,KAAK;IAEhB;IADpB,YACoB,QAAqB,EACrC,QAAe;QAEf,KAAK,CAAC,GAAG,QAAQ,CAAC,KAAK,YAAY,QAAQ,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAH5D,aAAQ,GAAR,QAAQ,CAAa;QAIrC,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,OAAO,KAAK,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;IACtF,CAAC;IAED;;;OAGG;IACH,4FAA4F;IACpF,MAAM,CAAC,QAAQ,CAAC,QAAqB;QACzC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,KAAK,QAAQ,CAAC,OAAO,EAAE,CAAC;QACvD,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,CAAC;CACJ;AAvBD,sCAuBC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAa,oBAAoB;IAET;IACA;IAFpB,YACoB,KAAoB,EACpB,WAA8B;QAD9B,UAAK,GAAL,KAAK,CAAe;QACpB,gBAAW,GAAX,WAAW,CAAmB;IAC/C,CAAC;IAEJ;;;OAGG;IACH,oLAAoL;IACpL,MAAM,CAAC,iBAAiB,CAAC,OAAgB;QACrC,MAAM,QAAQ,GAAG,yBAAW,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;QACzC,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,YAAY,KAAK,CAAC,EAAE,CAAC;YACxD,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,OAAO,IAAI,oBAAoB,CAAC,IAAI,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE;YAClE,OAAO,CAAC,IAAI;YACZ,QAAQ,CAAC,KAAK;SACjB,CAAC,CAAC;IACP,CAAC;CACJ;AArBD,oDAqBC","sourcesContent":["import { ApiCallSite } from './ApiCallSite';\n\n/**\n * The report-time wrapper for a failed api call: named `RpcCallFailed`, message\n * `Api.method failed: <original message>`, the CALL SITE's stack, and `cause` = the original error.\n *\n * NEVER THROWN. It exists only to be handed to an error reporter, so that the reported stack is the\n * app line that made the call while the original failure (and its own framework stack) is still\n * one `cause` hop away. Here `cause` is honest — the call failed BECAUSE of the original error —\n * which is exactly why the call site itself is not stored as the original's `cause`.\n */\nexport class RpcCallFailed extends Error {\n constructor(\n public readonly callSite: ApiCallSite,\n original: Error,\n ) {\n super(`${callSite.label} failed: ${original.message}`, { cause: original });\n this.name = 'RpcCallFailed';\n this.stack = `${this.name}: ${this.message}\\n${RpcCallFailed.framesOf(callSite)}`;\n }\n\n /**\n * The call site's frames without its own header line. V8 prefixes a stack with\n * `Name: message`; SpiderMonkey and JavaScriptCore do not, so only a matching first line is cut.\n */\n // webpieces-disable no-function-outside-class -- pure stack-text helper owned by this class\n private static framesOf(callSite: ApiCallSite): string {\n const stack = callSite.stack ?? '';\n const header = `${callSite.name}: ${callSite.message}`;\n if (!stack.startsWith(header)) {\n return stack;\n }\n return stack.slice(header.length).replace(/^\\n/, '');\n }\n}\n\n/**\n * What an error reporter needs to report a failed api call well: the {@link RpcCallFailed} wrapper\n * and a suggested grouping `fingerprint` of `[original.name, 'Api.method']`, so failures group per\n * endpoint and error type instead of collapsing into one issue for the whole app.\n *\n * Reporter-agnostic on purpose — webpieces does not depend on any error-reporting SDK. A Sentry app,\n * for example, would do:\n *\n * ```ts\n * const report = ReportableApiFailure.toReportableError(err);\n * if (report) Sentry.captureException(report.error, { fingerprint: [...report.fingerprint] });\n * else Sentry.captureException(err);\n * ```\n */\nexport class ReportableApiFailure {\n constructor(\n public readonly error: RpcCallFailed,\n public readonly fingerprint: readonly string[],\n ) {}\n\n /**\n * The reportable form of `failure`, or undefined when it carries no call site (it did not come\n * out of a generated api client, so there is nothing to add — report it as it is).\n */\n // webpieces-disable no-function-outside-class -- stateless factory over a caught value; webpieces-disable no-any-unknown -- a caught value is genuinely unknown until narrowed here\n static toReportableError(failure: unknown): ReportableApiFailure | undefined {\n const callSite = ApiCallSite.of(failure);\n if (callSite === undefined || !(failure instanceof Error)) {\n return undefined;\n }\n return new ReportableApiFailure(new RpcCallFailed(callSite, failure), [\n failure.name,\n callSite.label,\n ]);\n }\n}\n"]}
|
package/src/errors/index.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export { EndUserErrorCode } from './EndUserErrorCode';
|
|
|
4
4
|
export { EndUserErrorRegistry } from './EndUserErrorRegistry';
|
|
5
5
|
export type { EndUserErrorFactory } from './EndUserErrorRegistry';
|
|
6
6
|
export { ClientRole } from './ClientRole';
|
|
7
|
+
export { ApiCallSite } from './ApiCallSite';
|
|
8
|
+
export type { CallSiteBoundary } from './ApiCallSite';
|
|
9
|
+
export { ReportableApiFailure, RpcCallFailed } from './ReportableApiFailure';
|
|
7
10
|
export type { ApiErrorKind, ApiStatusCode, EdgeHttpStatus } from './ApiError';
|
|
8
11
|
export { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';
|
|
9
12
|
export { ApiErrorBoundary } from './ApiErrorBoundary';
|
package/src/errors/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.ReceivedApiErrorRule = exports.ApiErrorBoundary = exports.ApiErrorPayload = exports.ApiErrorCodec = exports.ClientRole = exports.EndUserErrorRegistry = exports.EndUserErrorCode = exports.ApiEndUserBadRequestError = exports.ApiEndUserForbiddenError = exports.ApiEndUserNotFoundError = exports.ApiClientTooOldError = exports.ApiCodedError = exports.ApiNotImplementedError = exports.ApiUnsupportedMediaTypeError = exports.ApiPreconditionFailedError = exports.ApiUnprocessableError = exports.ApiConflictError = exports.ApiDependencyBackoffError = exports.ApiUnauthorizedError = exports.ApiBadRequestError = exports.ApiEndUserError = exports.ApiEndpointNotFoundError = exports.ApiConnectionError = exports.ApiRequestTimeoutError = exports.ApiForbiddenError = exports.ApiRateLimitedError = exports.ApiImplementationError = exports.ApiDependencyTimeoutError = exports.ApiUnavailableError = exports.ApiBadGatewayError = exports.ApiDependencyError = exports.ApiNotFoundError = exports.ApiError = void 0;
|
|
3
|
+
exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.ReceivedApiErrorRule = exports.ApiErrorBoundary = exports.ApiErrorPayload = exports.ApiErrorCodec = exports.RpcCallFailed = exports.ReportableApiFailure = exports.ApiCallSite = exports.ClientRole = exports.EndUserErrorRegistry = exports.EndUserErrorCode = exports.ApiEndUserBadRequestError = exports.ApiEndUserForbiddenError = exports.ApiEndUserNotFoundError = exports.ApiClientTooOldError = exports.ApiCodedError = exports.ApiNotImplementedError = exports.ApiUnsupportedMediaTypeError = exports.ApiPreconditionFailedError = exports.ApiUnprocessableError = exports.ApiConflictError = exports.ApiDependencyBackoffError = exports.ApiUnauthorizedError = exports.ApiBadRequestError = exports.ApiEndUserError = exports.ApiEndpointNotFoundError = exports.ApiConnectionError = exports.ApiRequestTimeoutError = exports.ApiForbiddenError = exports.ApiRateLimitedError = exports.ApiImplementationError = exports.ApiDependencyTimeoutError = exports.ApiUnavailableError = exports.ApiBadGatewayError = exports.ApiDependencyError = exports.ApiNotFoundError = exports.ApiError = void 0;
|
|
4
4
|
var ApiError_1 = require("./ApiError");
|
|
5
5
|
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return ApiError_1.ApiError; } });
|
|
6
6
|
Object.defineProperty(exports, "ApiNotFoundError", { enumerable: true, get: function () { return ApiError_1.ApiNotFoundError; } });
|
|
@@ -35,6 +35,11 @@ var EndUserErrorRegistry_1 = require("./EndUserErrorRegistry");
|
|
|
35
35
|
Object.defineProperty(exports, "EndUserErrorRegistry", { enumerable: true, get: function () { return EndUserErrorRegistry_1.EndUserErrorRegistry; } });
|
|
36
36
|
var ClientRole_1 = require("./ClientRole");
|
|
37
37
|
Object.defineProperty(exports, "ClientRole", { enumerable: true, get: function () { return ClientRole_1.ClientRole; } });
|
|
38
|
+
var ApiCallSite_1 = require("./ApiCallSite");
|
|
39
|
+
Object.defineProperty(exports, "ApiCallSite", { enumerable: true, get: function () { return ApiCallSite_1.ApiCallSite; } });
|
|
40
|
+
var ReportableApiFailure_1 = require("./ReportableApiFailure");
|
|
41
|
+
Object.defineProperty(exports, "ReportableApiFailure", { enumerable: true, get: function () { return ReportableApiFailure_1.ReportableApiFailure; } });
|
|
42
|
+
Object.defineProperty(exports, "RpcCallFailed", { enumerable: true, get: function () { return ReportableApiFailure_1.RpcCallFailed; } });
|
|
38
43
|
var ApiErrorCodec_1 = require("./ApiErrorCodec");
|
|
39
44
|
Object.defineProperty(exports, "ApiErrorCodec", { enumerable: true, get: function () { return ApiErrorCodec_1.ApiErrorCodec; } });
|
|
40
45
|
Object.defineProperty(exports, "ApiErrorPayload", { enumerable: true, get: function () { return ApiErrorCodec_1.ApiErrorPayload; } });
|
package/src/errors/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/index.ts"],"names":[],"mappings":";;;AAAA,uCAwBoB;AAvBhB,oGAAA,QAAQ,OAAA;AACR,4GAAA,gBAAgB,OAAA;AAChB,8GAAA,kBAAkB,OAAA;AAClB,8GAAA,kBAAkB,OAAA;AAClB,+GAAA,mBAAmB,OAAA;AACnB,qHAAA,yBAAyB,OAAA;AACzB,kHAAA,sBAAsB,OAAA;AACtB,+GAAA,mBAAmB,OAAA;AACnB,6GAAA,iBAAiB,OAAA;AACjB,kHAAA,sBAAsB,OAAA;AACtB,8GAAA,kBAAkB,OAAA;AAClB,oHAAA,wBAAwB,OAAA;AACxB,2GAAA,eAAe,OAAA;AACf,8GAAA,kBAAkB,OAAA;AAClB,gHAAA,oBAAoB,OAAA;AACpB,qHAAA,yBAAyB,OAAA;AACzB,4GAAA,gBAAgB,OAAA;AAChB,iHAAA,qBAAqB,OAAA;AACrB,sHAAA,0BAA0B,OAAA;AAC1B,wHAAA,4BAA4B,OAAA;AAC5B,kHAAA,sBAAsB,OAAA;AACtB,yGAAA,aAAa,OAAA;AACb,gHAAA,oBAAoB,OAAA;AAExB,iDAIyB;AAHrB,wHAAA,uBAAuB,OAAA;AACvB,yHAAA,wBAAwB,OAAA;AACxB,0HAAA,yBAAyB,OAAA;AAE7B,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAE7B,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/index.ts"],"names":[],"mappings":";;;AAAA,uCAwBoB;AAvBhB,oGAAA,QAAQ,OAAA;AACR,4GAAA,gBAAgB,OAAA;AAChB,8GAAA,kBAAkB,OAAA;AAClB,8GAAA,kBAAkB,OAAA;AAClB,+GAAA,mBAAmB,OAAA;AACnB,qHAAA,yBAAyB,OAAA;AACzB,kHAAA,sBAAsB,OAAA;AACtB,+GAAA,mBAAmB,OAAA;AACnB,6GAAA,iBAAiB,OAAA;AACjB,kHAAA,sBAAsB,OAAA;AACtB,8GAAA,kBAAkB,OAAA;AAClB,oHAAA,wBAAwB,OAAA;AACxB,2GAAA,eAAe,OAAA;AACf,8GAAA,kBAAkB,OAAA;AAClB,gHAAA,oBAAoB,OAAA;AACpB,qHAAA,yBAAyB,OAAA;AACzB,4GAAA,gBAAgB,OAAA;AAChB,iHAAA,qBAAqB,OAAA;AACrB,sHAAA,0BAA0B,OAAA;AAC1B,wHAAA,4BAA4B,OAAA;AAC5B,kHAAA,sBAAsB,OAAA;AACtB,yGAAA,aAAa,OAAA;AACb,gHAAA,oBAAoB,OAAA;AAExB,iDAIyB;AAHrB,wHAAA,uBAAuB,OAAA;AACvB,yHAAA,wBAAwB,OAAA;AACxB,0HAAA,yBAAyB,OAAA;AAE7B,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAE7B,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AAEpB,+DAA6E;AAApE,4HAAA,oBAAoB,OAAA;AAAE,qHAAA,aAAa,OAAA;AAE5C,iDAAiE;AAAxD,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AACvC,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,yDAS6B;AARzB,qHAAA,gBAAgB,OAAA;AAChB,qHAAA,gBAAgB,OAAA;AAChB,gHAAA,WAAW,OAAA;AACX,iHAAA,YAAY,OAAA;AACZ,wHAAA,mBAAmB,OAAA;AACnB,iHAAA,YAAY,OAAA;AACZ,kHAAA,aAAa,OAAA;AACb,gHAAA,WAAW,OAAA","sourcesContent":["export {\n ApiError,\n ApiNotFoundError,\n ApiDependencyError,\n ApiBadGatewayError,\n ApiUnavailableError,\n ApiDependencyTimeoutError,\n ApiImplementationError,\n ApiRateLimitedError,\n ApiForbiddenError,\n ApiRequestTimeoutError,\n ApiConnectionError,\n ApiEndpointNotFoundError,\n ApiEndUserError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n ApiDependencyBackoffError,\n ApiConflictError,\n ApiUnprocessableError,\n ApiPreconditionFailedError,\n ApiUnsupportedMediaTypeError,\n ApiNotImplementedError,\n ApiCodedError,\n ApiClientTooOldError,\n} from './ApiError';\nexport {\n ApiEndUserNotFoundError,\n ApiEndUserForbiddenError,\n ApiEndUserBadRequestError,\n} from './EndUserErrors';\nexport { EndUserErrorCode } from './EndUserErrorCode';\nexport { EndUserErrorRegistry } from './EndUserErrorRegistry';\nexport type { EndUserErrorFactory } from './EndUserErrorRegistry';\nexport { ClientRole } from './ClientRole';\nexport { ApiCallSite } from './ApiCallSite';\nexport type { CallSiteBoundary } from './ApiCallSite';\nexport { ReportableApiFailure, RpcCallFailed } from './ReportableApiFailure';\nexport type { ApiErrorKind, ApiStatusCode, EdgeHttpStatus } from './ApiError';\nexport { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';\nexport { ApiErrorBoundary } from './ApiErrorBoundary';\nexport { ReceivedApiErrorRule } from './ReceivedApiErrorRule';\nexport {\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './AuthErrorSubtypes';\n"]}
|
|
@@ -42,6 +42,9 @@ export declare class WebpiecesDefaultErrorTranslator implements ErrorTranslator
|
|
|
42
42
|
* error; unknown throws and caller-local failures come out as a 500 implementation failure
|
|
43
43
|
* because that is what the payload says.
|
|
44
44
|
*
|
|
45
|
+
* The one exception is an {@link OAuthProtocolError}: its caller is an OAuth client, so it is
|
|
46
|
+
* answered in the RFC shape by `OAuthErrorResponse`, not as a payload.
|
|
47
|
+
*
|
|
45
48
|
* An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that
|
|
46
49
|
* as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request
|
|
47
50
|
* from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —
|
|
@@ -5,6 +5,8 @@ const ApiErrorBoundary_1 = require("../errors/ApiErrorBoundary");
|
|
|
5
5
|
const ApiErrorCodec_1 = require("../errors/ApiErrorCodec");
|
|
6
6
|
const ApiError_1 = require("../errors/ApiError");
|
|
7
7
|
const ReceivedApiErrorRule_1 = require("../errors/ReceivedApiErrorRule");
|
|
8
|
+
const OAuthErrorResponse_1 = require("../oauth/OAuthErrorResponse");
|
|
9
|
+
const OAuthProtocolError_1 = require("../oauth/OAuthProtocolError");
|
|
8
10
|
const ApiCallTimeoutError_1 = require("./ApiCallTimeoutError");
|
|
9
11
|
const ApiErrorHttpStatus_1 = require("./ApiErrorHttpStatus");
|
|
10
12
|
const HttpResponseDto_1 = require("./HttpResponseDto");
|
|
@@ -67,6 +69,9 @@ class WebpiecesDefaultErrorTranslator {
|
|
|
67
69
|
* error; unknown throws and caller-local failures come out as a 500 implementation failure
|
|
68
70
|
* because that is what the payload says.
|
|
69
71
|
*
|
|
72
|
+
* The one exception is an {@link OAuthProtocolError}: its caller is an OAuth client, so it is
|
|
73
|
+
* answered in the RFC shape by `OAuthErrorResponse`, not as a payload.
|
|
74
|
+
*
|
|
70
75
|
* An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that
|
|
71
76
|
* as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request
|
|
72
77
|
* from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —
|
|
@@ -75,6 +80,11 @@ class WebpiecesDefaultErrorTranslator {
|
|
|
75
80
|
* responsible for repeating its router's mode, and repeating it wrongly was silent.
|
|
76
81
|
*/
|
|
77
82
|
toWire(error) {
|
|
83
|
+
// An OAuth refusal is answered to an OAuth CLIENT, which parses the RFC 6749/6750 body and
|
|
84
|
+
// never the ApiError envelope (#1176). Same renderer as every other OAuth boundary.
|
|
85
|
+
if (error instanceof OAuthProtocolError_1.OAuthProtocolError) {
|
|
86
|
+
return OAuthErrorResponse_1.OAuthErrorResponse.toResponse(error, undefined);
|
|
87
|
+
}
|
|
78
88
|
const payload = this.boundary.encode(error);
|
|
79
89
|
// The boundary never publishes kind 'connection' — that is the whole first rule above — so
|
|
80
90
|
// this is the published subset ApiErrorHttpStatus.codeFor accepts.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesDefaultErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiErrorBoundary } from '../errors/ApiErrorBoundary';\nimport { ApiErrorCodec } from '../errors/ApiErrorCodec';\nimport { ApiCodedError, ApiError } from '../errors/ApiError';\nimport { ClientRole } from '../errors/ClientRole';\nimport { ReceivedApiErrorRule } from '../errors/ReceivedApiErrorRule';\nimport { ApiCallTimeoutError } from './ApiCallTimeoutError';\nimport { ApiErrorHttpStatus, PublishedKind } from './ApiErrorHttpStatus';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\n\n/**\n * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by\n * every node client and by the browser bundle alike.\n *\n * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is\n * what an app's own translator calls to DECLINE an error:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * so \"no translator registered\" and \"a translator that does not claim this error\" produce\n * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is\n * the package every API contract already depends on and the only one both a server and a browser\n * bundle can reach. Both halves needed nothing else: the outbound half wants\n * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants\n * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.\n *\n * It holds no ROLE: who is receiving ({@link ClientRole}) is an argument of `fromWire`, supplied by\n * the client that read the response from its own setup, so one stateless instance serves a server's\n * clients and a browser's alike.\n *\n * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing\n * your own costs nothing and reads better at a delegation site.\n */\nexport class WebpiecesDefaultErrorTranslator implements ErrorTranslator {\n /** Reason phrase for a client deadline; see {@link ApiCallTimeoutError}. Never 'Gateway Timeout'. */\n static readonly CALL_TIMEOUT_REASON = 'Network Timeout';\n\n private readonly boundary = new ApiErrorBoundary();\n\n private readonly genericMessages: Map<number, string> = new Map<number, string>([\n [266, 'End User Error'],\n [400, 'Bad Request'],\n [401, 'Unauthorized'],\n [403, 'Forbidden'],\n [404, 'Not Found'],\n [408, 'Request Timeout'],\n [409, 'Conflict'],\n [412, 'Precondition Failed'],\n [415, 'Unsupported Media Type'],\n [422, 'Unprocessable Content'],\n [426, 'Upgrade Required'],\n [429, 'Too Many Requests'],\n [500, 'Internal Server Error'],\n [501, 'Not Implemented'],\n [502, 'Bad Gateway'],\n [503, 'Service Unavailable'],\n [504, 'Gateway Timeout'],\n ]);\n\n /**\n * SERVER: an exception in, the exact HTTP response out.\n *\n * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared\n * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as\n * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload\n * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and\n * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw\n * error; unknown throws and caller-local failures come out as a 500 implementation failure\n * because that is what the payload says.\n *\n * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that\n * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request\n * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —\n * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any\n * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator\n * responsible for repeating its router's mode, and repeating it wrongly was silent.\n */\n toWire(error: Error): HttpResponseDto {\n const payload = this.boundary.encode(error);\n // The boundary never publishes kind 'connection' — that is the whole first rule above — so\n // this is the published subset ApiErrorHttpStatus.codeFor accepts.\n const kind = payload.kind as PublishedKind;\n const status = ApiErrorHttpStatus.codeFor(kind, payload.statusCode);\n const headers =\n payload.retryAfterSeconds !== undefined\n ? [new HttpHeader('retry-after', String(payload.retryAfterSeconds))]\n : [];\n return new HttpResponseDto(\n new HttpResponseStatus(status, this.reasonPhrase(error, status)),\n headers,\n payload,\n );\n }\n\n /**\n * The reason phrase for `error`. An {@link ApiCallTimeoutError} is THIS process's own client\n * deadline firing on an outbound call: no gateway was involved, so it is never titled\n * 'Gateway Timeout' (issue #1170). It keeps its 504 status and `dependency-timeout` payload —\n * retry semantics are unchanged — and only the human framing differs.\n */\n private reasonPhrase(error: Error, status: number): string {\n if (error instanceof ApiCallTimeoutError) {\n return WebpiecesDefaultErrorTranslator.CALL_TIMEOUT_REASON;\n }\n return this.genericMessage(status);\n }\n\n /**\n * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.\n *\n * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is\n * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller\n * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply\n * stop — a failure response can never fall through to a typed caller.\n *\n * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY\n * bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/\n * backoff/unavailable meaning, 266 is the end user's own answer, a 426 `client-too-old` stays\n * {@link ApiClientTooOldError}, and a 401 depends on `role` (see {@link ClientRole}).\n *\n * @param role - who is receiving, declared at the client's setup. No default.\n */\n fromWire(response: HttpResponseDto, role: ClientRole): void {\n const code = response.status.code;\n if (code >= 200 && code < 300 && code !== 266) {\n return;\n }\n const decoded = this.decodeBody(response);\n const message =\n decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);\n throw ReceivedApiErrorRule.adapt(\n role,\n code,\n message,\n decoded,\n this.retryAfterSeconds(response.headers),\n );\n }\n\n /** The reason phrase webpieces writes beside a status it chose itself. */\n genericMessage(code: number): string {\n return this.genericMessages.get(code) ?? 'Request Failed';\n }\n\n /**\n * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.\n * The status is only believed when it AGREES with the kind the body claims — a body saying\n * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.\n */\n private decodeBody(response: HttpResponseDto): ApiError | undefined {\n if (!ApiErrorCodec.isPayload(response.body)) {\n return undefined;\n }\n const decoded = ApiErrorCodec.decode(response.body);\n if (!ApiErrorHttpStatus.hasCode(decoded)) {\n return undefined;\n }\n // `statusCode` is the 'coded' kind's own status, the one thing the shared table cannot know.\n const statusCode = decoded instanceof ApiCodedError ? decoded.statusCode : undefined;\n const declared = ApiErrorHttpStatus.codeFor(decoded.kind, statusCode);\n return declared === response.status.code ? decoded : undefined;\n }\n\n // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them\n private fallbackMessage(body: unknown, reason: string): string {\n if (typeof body === 'object' && body !== null) {\n const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;\n if (typeof message === 'string' && message.length > 0) return message.slice(0, 4096);\n }\n return reason || 'Request Failed';\n }\n\n /** Parse either legal Retry-After form and bound untrusted guidance to one day. */\n private retryAfterSeconds(headers: readonly HttpHeader[]): number | undefined {\n const raw = headers.find(\n (header: HttpHeader) => header.name.toLowerCase() === 'retry-after',\n )?.value;\n if (raw === undefined) return undefined;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.min(Math.ceil(seconds), 86_400);\n const at = Date.parse(raw);\n if (!Number.isFinite(at)) return undefined;\n return Math.min(Math.max(Math.ceil((at - Date.now()) / 1_000), 0), 86_400);\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_TRANSLATOR = new WebpiecesDefaultErrorTranslator();\n"]}
|
|
1
|
+
{"version":3,"file":"WebpiecesDefaultErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiErrorBoundary } from '../errors/ApiErrorBoundary';\nimport { ApiErrorCodec } from '../errors/ApiErrorCodec';\nimport { ApiCodedError, ApiError } from '../errors/ApiError';\nimport { ClientRole } from '../errors/ClientRole';\nimport { ReceivedApiErrorRule } from '../errors/ReceivedApiErrorRule';\nimport { OAuthErrorResponse } from '../oauth/OAuthErrorResponse';\nimport { OAuthProtocolError } from '../oauth/OAuthProtocolError';\nimport { ApiCallTimeoutError } from './ApiCallTimeoutError';\nimport { ApiErrorHttpStatus, PublishedKind } from './ApiErrorHttpStatus';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\n\n/**\n * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by\n * every node client and by the browser bundle alike.\n *\n * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is\n * what an app's own translator calls to DECLINE an error:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * so \"no translator registered\" and \"a translator that does not claim this error\" produce\n * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is\n * the package every API contract already depends on and the only one both a server and a browser\n * bundle can reach. Both halves needed nothing else: the outbound half wants\n * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants\n * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.\n *\n * It holds no ROLE: who is receiving ({@link ClientRole}) is an argument of `fromWire`, supplied by\n * the client that read the response from its own setup, so one stateless instance serves a server's\n * clients and a browser's alike.\n *\n * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing\n * your own costs nothing and reads better at a delegation site.\n */\nexport class WebpiecesDefaultErrorTranslator implements ErrorTranslator {\n /** Reason phrase for a client deadline; see {@link ApiCallTimeoutError}. Never 'Gateway Timeout'. */\n static readonly CALL_TIMEOUT_REASON = 'Network Timeout';\n\n private readonly boundary = new ApiErrorBoundary();\n\n private readonly genericMessages: Map<number, string> = new Map<number, string>([\n [266, 'End User Error'],\n [400, 'Bad Request'],\n [401, 'Unauthorized'],\n [403, 'Forbidden'],\n [404, 'Not Found'],\n [408, 'Request Timeout'],\n [409, 'Conflict'],\n [412, 'Precondition Failed'],\n [415, 'Unsupported Media Type'],\n [422, 'Unprocessable Content'],\n [426, 'Upgrade Required'],\n [429, 'Too Many Requests'],\n [500, 'Internal Server Error'],\n [501, 'Not Implemented'],\n [502, 'Bad Gateway'],\n [503, 'Service Unavailable'],\n [504, 'Gateway Timeout'],\n ]);\n\n /**\n * SERVER: an exception in, the exact HTTP response out.\n *\n * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared\n * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as\n * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload\n * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and\n * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw\n * error; unknown throws and caller-local failures come out as a 500 implementation failure\n * because that is what the payload says.\n *\n * The one exception is an {@link OAuthProtocolError}: its caller is an OAuth client, so it is\n * answered in the RFC shape by `OAuthErrorResponse`, not as a payload.\n *\n * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that\n * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request\n * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —\n * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any\n * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator\n * responsible for repeating its router's mode, and repeating it wrongly was silent.\n */\n toWire(error: Error): HttpResponseDto {\n // An OAuth refusal is answered to an OAuth CLIENT, which parses the RFC 6749/6750 body and\n // never the ApiError envelope (#1176). Same renderer as every other OAuth boundary.\n if (error instanceof OAuthProtocolError) {\n return OAuthErrorResponse.toResponse(error, undefined);\n }\n const payload = this.boundary.encode(error);\n // The boundary never publishes kind 'connection' — that is the whole first rule above — so\n // this is the published subset ApiErrorHttpStatus.codeFor accepts.\n const kind = payload.kind as PublishedKind;\n const status = ApiErrorHttpStatus.codeFor(kind, payload.statusCode);\n const headers =\n payload.retryAfterSeconds !== undefined\n ? [new HttpHeader('retry-after', String(payload.retryAfterSeconds))]\n : [];\n return new HttpResponseDto(\n new HttpResponseStatus(status, this.reasonPhrase(error, status)),\n headers,\n payload,\n );\n }\n\n /**\n * The reason phrase for `error`. An {@link ApiCallTimeoutError} is THIS process's own client\n * deadline firing on an outbound call: no gateway was involved, so it is never titled\n * 'Gateway Timeout' (issue #1170). It keeps its 504 status and `dependency-timeout` payload —\n * retry semantics are unchanged — and only the human framing differs.\n */\n private reasonPhrase(error: Error, status: number): string {\n if (error instanceof ApiCallTimeoutError) {\n return WebpiecesDefaultErrorTranslator.CALL_TIMEOUT_REASON;\n }\n return this.genericMessage(status);\n }\n\n /**\n * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.\n *\n * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is\n * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller\n * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply\n * stop — a failure response can never fall through to a typed caller.\n *\n * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY\n * bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/\n * backoff/unavailable meaning, 266 is the end user's own answer, a 426 `client-too-old` stays\n * {@link ApiClientTooOldError}, and a 401 depends on `role` (see {@link ClientRole}).\n *\n * @param role - who is receiving, declared at the client's setup. No default.\n */\n fromWire(response: HttpResponseDto, role: ClientRole): void {\n const code = response.status.code;\n if (code >= 200 && code < 300 && code !== 266) {\n return;\n }\n const decoded = this.decodeBody(response);\n const message =\n decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);\n throw ReceivedApiErrorRule.adapt(\n role,\n code,\n message,\n decoded,\n this.retryAfterSeconds(response.headers),\n );\n }\n\n /** The reason phrase webpieces writes beside a status it chose itself. */\n genericMessage(code: number): string {\n return this.genericMessages.get(code) ?? 'Request Failed';\n }\n\n /**\n * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.\n * The status is only believed when it AGREES with the kind the body claims — a body saying\n * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.\n */\n private decodeBody(response: HttpResponseDto): ApiError | undefined {\n if (!ApiErrorCodec.isPayload(response.body)) {\n return undefined;\n }\n const decoded = ApiErrorCodec.decode(response.body);\n if (!ApiErrorHttpStatus.hasCode(decoded)) {\n return undefined;\n }\n // `statusCode` is the 'coded' kind's own status, the one thing the shared table cannot know.\n const statusCode = decoded instanceof ApiCodedError ? decoded.statusCode : undefined;\n const declared = ApiErrorHttpStatus.codeFor(decoded.kind, statusCode);\n return declared === response.status.code ? decoded : undefined;\n }\n\n // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them\n private fallbackMessage(body: unknown, reason: string): string {\n if (typeof body === 'object' && body !== null) {\n const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;\n if (typeof message === 'string' && message.length > 0) return message.slice(0, 4096);\n }\n return reason || 'Request Failed';\n }\n\n /** Parse either legal Retry-After form and bound untrusted guidance to one day. */\n private retryAfterSeconds(headers: readonly HttpHeader[]): number | undefined {\n const raw = headers.find(\n (header: HttpHeader) => header.name.toLowerCase() === 'retry-after',\n )?.value;\n if (raw === undefined) return undefined;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.min(Math.ceil(seconds), 86_400);\n const at = Date.parse(raw);\n if (!Number.isFinite(at)) return undefined;\n return Math.min(Math.max(Math.ceil((at - Date.now()) / 1_000), 0), 86_400);\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_TRANSLATOR = new WebpiecesDefaultErrorTranslator();\n"]}
|
|
@@ -16,8 +16,9 @@ import { FailureClassifier } from './FailureClassifier';
|
|
|
16
16
|
* - ApiBadRequestError (400), ApiUnauthorizedError (401), ApiForbiddenError (403),
|
|
17
17
|
* ApiNotFoundError (404), ApiConflictError (409), ApiPreconditionFailedError (412),
|
|
18
18
|
* ApiUnsupportedMediaTypeError (415), ApiUnprocessableError (422), and an ApiCodedError whose
|
|
19
|
-
* `isCallerError()` is true (below 500, except 408/429)
|
|
20
|
-
*
|
|
19
|
+
* `isCallerError()` is true (below 500, except 408/429), and an OAuthProtocolError below 500 (a
|
|
20
|
+
* refused grant or token is the OAuth protocol working, #1176) → the server is fine, the caller
|
|
21
|
+
* erred → NON-failure.
|
|
21
22
|
* SERVER — something may actually be WRONG, so SURFACE it (failure):
|
|
22
23
|
* - ApiRequestTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately
|
|
23
24
|
* absent below, so it counts as a failure. Implementation/dependency failures, ApiNotImplementedError
|