@webpieces/core-util 0.4.784 → 0.4.786
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
CHANGED
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
import { Logger } from '../logging/Logger';
|
|
2
|
+
import { ApiErrorPayload } from './ApiErrorCodec';
|
|
2
3
|
import { ApiError } from './ApiError';
|
|
3
4
|
/**
|
|
4
5
|
* The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before
|
|
5
6
|
* rendering it, plus the one operator log line per failure. Adapters differ only in the wire shape
|
|
6
|
-
* they render;
|
|
7
|
+
* they render; classification and log levels live here so they cannot drift apart.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here SUBSTITUTES an object for the value that was thrown: the thrown error survives to
|
|
10
|
+
* every consumer, so an app can still `instanceof` its own error class and the operator log still
|
|
11
|
+
* names it. Classification is a VALUE computed from the thrown error, never a wrapper around it.
|
|
7
12
|
*/
|
|
8
13
|
export declare class ApiErrorBoundary {
|
|
9
14
|
private readonly log;
|
|
10
15
|
constructor(log: Logger);
|
|
11
16
|
/**
|
|
12
|
-
*
|
|
13
|
-
* a
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* The classified API outcome `thrown` publishes as, or `undefined` when it is a bug or broken
|
|
18
|
+
* state of THIS service — a non-`ApiError`, a non-`Error` throw, or a caller-local
|
|
19
|
+
* `ApiConnectionError`.
|
|
20
|
+
*
|
|
21
|
+
* A connection failure is OUR outbound call failing, so from our caller's seat this service is
|
|
22
|
+
* the thing that is broken: it publishes as `implementation`, never as `connection`. That rule
|
|
23
|
+
* is deliberately NOT in `ApiErrorCodec`, which encodes a value faithfully; it belongs to the
|
|
24
|
+
* boundary that owns the failure.
|
|
25
|
+
*
|
|
26
|
+
* The returned error IS the thrown error — never a replacement.
|
|
16
27
|
*/
|
|
17
|
-
|
|
28
|
+
apiOutcome(thrown: unknown): ApiError | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The wire payload THIS service publishes for `thrown`. Only an {@link ApiEndUserError} ever
|
|
31
|
+
* publishes its own message; everything else gets allowlisted generic text by kind.
|
|
32
|
+
*/
|
|
33
|
+
encode(thrown: unknown): ApiErrorPayload;
|
|
18
34
|
/**
|
|
19
35
|
* Logs one operator line at the per-kind level: expected caller outcomes at info, everything else
|
|
20
36
|
* at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).
|
|
37
|
+
*
|
|
38
|
+
* The line names the class that was ACTUALLY thrown, which is the single fact an operator needs
|
|
39
|
+
* and the one a wrapper would have destroyed.
|
|
21
40
|
*/
|
|
22
|
-
logOperatorDetail(
|
|
41
|
+
logOperatorDetail(thrown: unknown, correlation?: string): void;
|
|
23
42
|
private operatorDetail;
|
|
24
43
|
}
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ApiErrorBoundary = void 0;
|
|
4
4
|
const errorUtils_1 = require("../lib/errorUtils");
|
|
5
|
+
const ApiErrorCodec_1 = require("./ApiErrorCodec");
|
|
5
6
|
const ApiError_1 = require("./ApiError");
|
|
6
7
|
/**
|
|
7
8
|
* The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before
|
|
8
9
|
* rendering it, plus the one operator log line per failure. Adapters differ only in the wire shape
|
|
9
|
-
* they render;
|
|
10
|
+
* they render; classification and log levels live here so they cannot drift apart.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here SUBSTITUTES an object for the value that was thrown: the thrown error survives to
|
|
13
|
+
* every consumer, so an app can still `instanceof` its own error class and the operator log still
|
|
14
|
+
* names it. Classification is a VALUE computed from the thrown error, never a wrapper around it.
|
|
10
15
|
*/
|
|
11
16
|
class ApiErrorBoundary {
|
|
12
17
|
log;
|
|
@@ -14,53 +19,77 @@ class ApiErrorBoundary {
|
|
|
14
19
|
this.log = log;
|
|
15
20
|
}
|
|
16
21
|
/**
|
|
17
|
-
*
|
|
18
|
-
* a
|
|
19
|
-
*
|
|
20
|
-
*
|
|
22
|
+
* The classified API outcome `thrown` publishes as, or `undefined` when it is a bug or broken
|
|
23
|
+
* state of THIS service — a non-`ApiError`, a non-`Error` throw, or a caller-local
|
|
24
|
+
* `ApiConnectionError`.
|
|
25
|
+
*
|
|
26
|
+
* A connection failure is OUR outbound call failing, so from our caller's seat this service is
|
|
27
|
+
* the thing that is broken: it publishes as `implementation`, never as `connection`. That rule
|
|
28
|
+
* is deliberately NOT in `ApiErrorCodec`, which encodes a value faithfully; it belongs to the
|
|
29
|
+
* boundary that owns the failure.
|
|
30
|
+
*
|
|
31
|
+
* The returned error IS the thrown error — never a replacement.
|
|
21
32
|
*/
|
|
22
|
-
// webpieces-disable no-any-unknown -- thrown values are unknown until
|
|
23
|
-
|
|
33
|
+
// webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary
|
|
34
|
+
apiOutcome(thrown) {
|
|
24
35
|
if (thrown instanceof ApiError_1.ApiError && !(thrown instanceof ApiError_1.ApiConnectionError))
|
|
25
36
|
return thrown;
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The wire payload THIS service publishes for `thrown`. Only an {@link ApiEndUserError} ever
|
|
41
|
+
* publishes its own message; everything else gets allowlisted generic text by kind.
|
|
42
|
+
*/
|
|
43
|
+
// webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary
|
|
44
|
+
encode(thrown) {
|
|
45
|
+
const outcome = this.apiOutcome(thrown);
|
|
46
|
+
if (!outcome)
|
|
47
|
+
return new ApiErrorCodec_1.ApiErrorPayload('implementation', 'Internal Error');
|
|
48
|
+
return ApiErrorCodec_1.ApiErrorCodec.encode(outcome);
|
|
28
49
|
}
|
|
29
50
|
/**
|
|
30
51
|
* Logs one operator line at the per-kind level: expected caller outcomes at info, everything else
|
|
31
52
|
* at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).
|
|
53
|
+
*
|
|
54
|
+
* The line names the class that was ACTUALLY thrown, which is the single fact an operator needs
|
|
55
|
+
* and the one a wrapper would have destroyed.
|
|
32
56
|
*/
|
|
33
|
-
|
|
57
|
+
// webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary
|
|
58
|
+
logOperatorDetail(thrown, correlation) {
|
|
34
59
|
const suffix = correlation ? ` ${correlation}` : '';
|
|
35
|
-
const detail = `${this.operatorDetail(
|
|
36
|
-
if (
|
|
60
|
+
const detail = `${this.operatorDetail(thrown)}${suffix}`;
|
|
61
|
+
if (thrown instanceof ApiError_1.ApiEndUserError)
|
|
37
62
|
this.log.info(`End User Error: ${detail}`);
|
|
38
|
-
else if (
|
|
63
|
+
else if (thrown instanceof ApiError_1.ApiBadRequestError)
|
|
39
64
|
this.log.info(`Bad Request: ${detail}`);
|
|
40
|
-
else if (
|
|
65
|
+
else if (thrown instanceof ApiError_1.ApiNotFoundError)
|
|
41
66
|
this.log.info(`Not Found: ${detail}`);
|
|
42
|
-
else if (
|
|
67
|
+
else if (thrown instanceof ApiError_1.ApiUnauthorizedError)
|
|
43
68
|
this.log.info(`Unauthorized: ${detail}`);
|
|
44
|
-
else if (
|
|
69
|
+
else if (thrown instanceof ApiError_1.ApiForbiddenError)
|
|
45
70
|
this.log.info(`Forbidden: ${detail}`);
|
|
46
|
-
else if (
|
|
71
|
+
else if (thrown instanceof ApiError_1.ApiConflictError)
|
|
47
72
|
this.log.info(`Conflict: ${detail}`);
|
|
48
|
-
else if (
|
|
73
|
+
else if (thrown instanceof ApiError_1.ApiUnprocessableError)
|
|
49
74
|
this.log.info(`Unprocessable: ${detail}`);
|
|
50
|
-
else if (
|
|
75
|
+
else if (thrown instanceof ApiError_1.ApiPreconditionFailedError)
|
|
51
76
|
this.log.info(`Precondition Failed: ${detail}`);
|
|
52
|
-
else if (
|
|
77
|
+
else if (thrown instanceof ApiError_1.ApiUnsupportedMediaTypeError)
|
|
53
78
|
this.log.info(`Unsupported Media Type: ${detail}`);
|
|
54
|
-
else if (
|
|
55
|
-
this.log.info(`Coded ${
|
|
56
|
-
else if (
|
|
79
|
+
else if (thrown instanceof ApiError_1.ApiCodedError && thrown.isCallerError())
|
|
80
|
+
this.log.info(`Coded ${thrown.statusCode}: ${detail}`);
|
|
81
|
+
else if (thrown instanceof ApiError_1.ApiRequestTimeoutError)
|
|
57
82
|
this.log.error(`Request Timeout: ${detail}`);
|
|
58
83
|
else
|
|
59
84
|
this.log.error(`API failure: ${detail}`);
|
|
60
85
|
}
|
|
61
|
-
|
|
86
|
+
// webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary
|
|
87
|
+
operatorDetail(thrown) {
|
|
88
|
+
const error = (0, errorUtils_1.toError)(thrown);
|
|
89
|
+
const kind = this.apiOutcome(thrown)?.kind ?? 'implementation';
|
|
90
|
+
const subType = thrown instanceof ApiError_1.ApiError ? thrown.subType : undefined;
|
|
62
91
|
const cause = error.cause instanceof Error ? ` cause=${error.cause.message}` : '';
|
|
63
|
-
return `[name=${error.name} kind=${
|
|
92
|
+
return `[name=${error.name} kind=${kind} subType=${subType ?? 'none'}] ${error.message}${cause}`;
|
|
64
93
|
}
|
|
65
94
|
}
|
|
66
95
|
exports.ApiErrorBoundary = ApiErrorBoundary;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiErrorBoundary.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiErrorBoundary.ts"],"names":[],"mappings":";;;AACA,kDAA4C;AAC5C,
|
|
1
|
+
{"version":3,"file":"ApiErrorBoundary.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiErrorBoundary.ts"],"names":[],"mappings":"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","sourcesContent":["import { Logger } from '../logging/Logger';\nimport { toError } from '../lib/errorUtils';\nimport { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';\nimport {\n ApiBadRequestError,\n ApiCodedError,\n ApiConflictError,\n ApiConnectionError,\n ApiEndUserError,\n ApiError,\n ApiForbiddenError,\n ApiNotFoundError,\n ApiPreconditionFailedError,\n ApiRequestTimeoutError,\n ApiUnauthorizedError,\n ApiUnprocessableError,\n ApiUnsupportedMediaTypeError,\n} from './ApiError';\n\n/**\n * The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before\n * rendering it, plus the one operator log line per failure. Adapters differ only in the wire shape\n * they render; classification and log levels live here so they cannot drift apart.\n *\n * Nothing here SUBSTITUTES an object for the value that was thrown: the thrown error survives to\n * every consumer, so an app can still `instanceof` its own error class and the operator log still\n * names it. Classification is a VALUE computed from the thrown error, never a wrapper around it.\n */\nexport class ApiErrorBoundary {\n constructor(private readonly log: Logger) {}\n\n /**\n * The classified API outcome `thrown` publishes as, or `undefined` when it is a bug or broken\n * state of THIS service — a non-`ApiError`, a non-`Error` throw, or a caller-local\n * `ApiConnectionError`.\n *\n * A connection failure is OUR outbound call failing, so from our caller's seat this service is\n * the thing that is broken: it publishes as `implementation`, never as `connection`. That rule\n * is deliberately NOT in `ApiErrorCodec`, which encodes a value faithfully; it belongs to the\n * boundary that owns the failure.\n *\n * The returned error IS the thrown error — never a replacement.\n */\n // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary\n apiOutcome(thrown: unknown): ApiError | undefined {\n if (thrown instanceof ApiError && !(thrown instanceof ApiConnectionError)) return thrown;\n return undefined;\n }\n\n /**\n * The wire payload THIS service publishes for `thrown`. Only an {@link ApiEndUserError} ever\n * publishes its own message; everything else gets allowlisted generic text by kind.\n */\n // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary\n encode(thrown: unknown): ApiErrorPayload {\n const outcome = this.apiOutcome(thrown);\n if (!outcome) return new ApiErrorPayload('implementation', 'Internal Error');\n return ApiErrorCodec.encode(outcome);\n }\n\n /**\n * Logs one operator line at the per-kind level: expected caller outcomes at info, everything else\n * at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).\n *\n * The line names the class that was ACTUALLY thrown, which is the single fact an operator needs\n * and the one a wrapper would have destroyed.\n */\n // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary\n logOperatorDetail(thrown: unknown, correlation?: string): void {\n const suffix = correlation ? ` ${correlation}` : '';\n const detail = `${this.operatorDetail(thrown)}${suffix}`;\n if (thrown instanceof ApiEndUserError) this.log.info(`End User Error: ${detail}`);\n else if (thrown instanceof ApiBadRequestError) this.log.info(`Bad Request: ${detail}`);\n else if (thrown instanceof ApiNotFoundError) this.log.info(`Not Found: ${detail}`);\n else if (thrown instanceof ApiUnauthorizedError) this.log.info(`Unauthorized: ${detail}`);\n else if (thrown instanceof ApiForbiddenError) this.log.info(`Forbidden: ${detail}`);\n else if (thrown instanceof ApiConflictError) this.log.info(`Conflict: ${detail}`);\n else if (thrown instanceof ApiUnprocessableError) this.log.info(`Unprocessable: ${detail}`);\n else if (thrown instanceof ApiPreconditionFailedError)\n this.log.info(`Precondition Failed: ${detail}`);\n else if (thrown instanceof ApiUnsupportedMediaTypeError)\n this.log.info(`Unsupported Media Type: ${detail}`);\n else if (thrown instanceof ApiCodedError && thrown.isCallerError())\n this.log.info(`Coded ${thrown.statusCode}: ${detail}`);\n else if (thrown instanceof ApiRequestTimeoutError)\n this.log.error(`Request Timeout: ${detail}`);\n else this.log.error(`API failure: ${detail}`);\n }\n\n // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary\n private operatorDetail(thrown: unknown): string {\n const error = toError(thrown);\n const kind = this.apiOutcome(thrown)?.kind ?? 'implementation';\n const subType = thrown instanceof ApiError ? thrown.subType : undefined;\n const cause = error.cause instanceof Error ? ` cause=${error.cause.message}` : '';\n return `[name=${error.name} kind=${kind} subType=${subType ?? 'none'}] ${error.message}${cause}`;\n }\n}\n"]}
|