@webpieces/core-util 0.4.412 → 0.4.413
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/http/ClientRegistry.d.ts +42 -1
- package/src/http/ClientRegistry.js +65 -1
- package/src/http/ClientRegistry.js.map +1 -1
- package/src/http/FailureClassifier.d.ts +48 -0
- package/src/http/FailureClassifier.js +19 -0
- package/src/http/FailureClassifier.js.map +1 -0
- package/src/http/LogApiCall.d.ts +6 -22
- package/src/http/LogApiCall.js +15 -40
- package/src/http/LogApiCall.js.map +1 -1
- package/src/http/WebpiecesDefaultFailureClassifier.d.ts +35 -0
- package/src/http/WebpiecesDefaultFailureClassifier.js +55 -0
- package/src/http/WebpiecesDefaultFailureClassifier.js.map +1 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +6 -1
- package/src/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ErrorTranslation, ErrorWireForm } from './ErrorTranslation';
|
|
2
2
|
import { ProtocolError } from './errors';
|
|
3
|
+
import { FailureClassifier } from './FailureClassifier';
|
|
4
|
+
import { ApiMethodInfo } from './ApiMethodInfo';
|
|
3
5
|
/**
|
|
4
6
|
* Derives a base URL for a service name that has NO registered mapping — the pluggable half of
|
|
5
7
|
* {@link ClientRegistry} resolution. One per environment, installed once at startup:
|
|
@@ -54,6 +56,20 @@ export declare class ClientRegistry {
|
|
|
54
56
|
* work. See {@link ErrorTranslation}.
|
|
55
57
|
*/
|
|
56
58
|
private static readonly errorTranslations;
|
|
59
|
+
/**
|
|
60
|
+
* The app/company DEFAULT {@link FailureClassifier} — ONE per process, reads {@link ApiMethodInfo.side}
|
|
61
|
+
* so a single strategy covers the server router AND all internal clients. Undefined = use the
|
|
62
|
+
* webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}). Populated once at startup on the
|
|
63
|
+
* SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.
|
|
64
|
+
*/
|
|
65
|
+
private static appDefaultFailureClassifier;
|
|
66
|
+
/**
|
|
67
|
+
* Per-EXTERNAL-client {@link FailureClassifier}s, keyed by {@link ApiMethodInfo.apiClass}
|
|
68
|
+
* ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external
|
|
69
|
+
* client overrides the default for its own apiClass only. Internal clients / the server register
|
|
70
|
+
* nothing here and fall through to the default. See {@link ClientRegistry.classifyFailure}.
|
|
71
|
+
*/
|
|
72
|
+
private static readonly failureClassifiersByApiClass;
|
|
57
73
|
/** Map a service name to `http://localhost:<port>`. */
|
|
58
74
|
static addMapping(svcName: string, port: number): void;
|
|
59
75
|
/**
|
|
@@ -108,6 +124,31 @@ export declare class ClientRegistry {
|
|
|
108
124
|
* to the generic webpieces switch.
|
|
109
125
|
*/
|
|
110
126
|
static tryTranslateFromWire(statusCode: number, protocolError: ProtocolError): Error | undefined;
|
|
111
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Set the app/company DEFAULT failure classifier — ONE per process, covering the server router and
|
|
129
|
+
* all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the
|
|
130
|
+
* server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier
|
|
131
|
+
* claims. See {@link FailureClassifier}.
|
|
132
|
+
*/
|
|
133
|
+
static setDefaultFailureClassifier(classifier: FailureClassifier): void;
|
|
134
|
+
/**
|
|
135
|
+
* Register a per-EXTERNAL-client failure classifier, keyed by {@link ApiMethodInfo.apiClass}
|
|
136
|
+
* (e.g. 'FirestoreAdminClient'). Consulted BEFORE the default tier for calls with that apiClass.
|
|
137
|
+
* Call ONCE at startup. See {@link FailureClassifier}.
|
|
138
|
+
*/
|
|
139
|
+
static addFailureClassifier(apiClass: string, classifier: FailureClassifier): void;
|
|
140
|
+
/**
|
|
141
|
+
* Is this thrown API-call error a real FAILURE (true) or an expected non-failure (false)?
|
|
142
|
+
* Resolved most-specific-first, and ALWAYS definitive (returns a boolean):
|
|
143
|
+
* 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;
|
|
144
|
+
* 2. else the app default classifier, if one was set — unless it defers;
|
|
145
|
+
* 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}), which never defers.
|
|
146
|
+
* A `?? true` backstop keeps an unexpectedly-deferring built-in fail-safe (treat as failure).
|
|
147
|
+
*/
|
|
148
|
+
static classifyFailure(error: Error, methodInfo: ApiMethodInfo): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Reset mappings, the deriver, error translations, AND failure classifiers. For tests, so the
|
|
151
|
+
* process-globals do not leak across specs.
|
|
152
|
+
*/
|
|
112
153
|
static clear(): void;
|
|
113
154
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ClientRegistry = void 0;
|
|
4
|
+
const WebpiecesDefaultFailureClassifier_1 = require("./WebpiecesDefaultFailureClassifier");
|
|
4
5
|
/**
|
|
5
6
|
* ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every
|
|
6
7
|
* environment. A client is built once but a URL is per-environment, so the URL belongs here rather
|
|
@@ -42,6 +43,20 @@ class ClientRegistry {
|
|
|
42
43
|
* work. See {@link ErrorTranslation}.
|
|
43
44
|
*/
|
|
44
45
|
static errorTranslations = [];
|
|
46
|
+
/**
|
|
47
|
+
* The app/company DEFAULT {@link FailureClassifier} — ONE per process, reads {@link ApiMethodInfo.side}
|
|
48
|
+
* so a single strategy covers the server router AND all internal clients. Undefined = use the
|
|
49
|
+
* webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}). Populated once at startup on the
|
|
50
|
+
* SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.
|
|
51
|
+
*/
|
|
52
|
+
static appDefaultFailureClassifier;
|
|
53
|
+
/**
|
|
54
|
+
* Per-EXTERNAL-client {@link FailureClassifier}s, keyed by {@link ApiMethodInfo.apiClass}
|
|
55
|
+
* ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external
|
|
56
|
+
* client overrides the default for its own apiClass only. Internal clients / the server register
|
|
57
|
+
* nothing here and fall through to the default. See {@link ClientRegistry.classifyFailure}.
|
|
58
|
+
*/
|
|
59
|
+
static failureClassifiersByApiClass = new Map();
|
|
45
60
|
/** Map a service name to `http://localhost:<port>`. */
|
|
46
61
|
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
47
62
|
static addMapping(svcName, port) {
|
|
@@ -159,12 +174,61 @@ class ClientRegistry {
|
|
|
159
174
|
}
|
|
160
175
|
return undefined;
|
|
161
176
|
}
|
|
162
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Set the app/company DEFAULT failure classifier — ONE per process, covering the server router and
|
|
179
|
+
* all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the
|
|
180
|
+
* server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier
|
|
181
|
+
* claims. See {@link FailureClassifier}.
|
|
182
|
+
*/
|
|
183
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
184
|
+
static setDefaultFailureClassifier(classifier) {
|
|
185
|
+
ClientRegistry.appDefaultFailureClassifier = classifier;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Register a per-EXTERNAL-client failure classifier, keyed by {@link ApiMethodInfo.apiClass}
|
|
189
|
+
* (e.g. 'FirestoreAdminClient'). Consulted BEFORE the default tier for calls with that apiClass.
|
|
190
|
+
* Call ONCE at startup. See {@link FailureClassifier}.
|
|
191
|
+
*/
|
|
192
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
193
|
+
static addFailureClassifier(apiClass, classifier) {
|
|
194
|
+
ClientRegistry.failureClassifiersByApiClass.set(apiClass, classifier);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Is this thrown API-call error a real FAILURE (true) or an expected non-failure (false)?
|
|
198
|
+
* Resolved most-specific-first, and ALWAYS definitive (returns a boolean):
|
|
199
|
+
* 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;
|
|
200
|
+
* 2. else the app default classifier, if one was set — unless it defers;
|
|
201
|
+
* 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}), which never defers.
|
|
202
|
+
* A `?? true` backstop keeps an unexpectedly-deferring built-in fail-safe (treat as failure).
|
|
203
|
+
*/
|
|
204
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
205
|
+
static classifyFailure(error, methodInfo) {
|
|
206
|
+
const perClient = ClientRegistry.failureClassifiersByApiClass.get(methodInfo.apiClass);
|
|
207
|
+
if (perClient !== undefined) {
|
|
208
|
+
const verdict = perClient.isFailure(error, methodInfo);
|
|
209
|
+
if (verdict !== undefined) {
|
|
210
|
+
return verdict;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (ClientRegistry.appDefaultFailureClassifier !== undefined) {
|
|
214
|
+
const verdict = ClientRegistry.appDefaultFailureClassifier.isFailure(error, methodInfo);
|
|
215
|
+
if (verdict !== undefined) {
|
|
216
|
+
return verdict;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return WebpiecesDefaultFailureClassifier_1.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(error, methodInfo) ?? true;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Reset mappings, the deriver, error translations, AND failure classifiers. For tests, so the
|
|
223
|
+
* process-globals do not leak across specs.
|
|
224
|
+
*/
|
|
163
225
|
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
164
226
|
static clear() {
|
|
165
227
|
ClientRegistry.mappings.clear();
|
|
166
228
|
ClientRegistry.deriver = undefined;
|
|
167
229
|
ClientRegistry.errorTranslations.length = 0;
|
|
230
|
+
ClientRegistry.appDefaultFailureClassifier = undefined;
|
|
231
|
+
ClientRegistry.failureClassifiersByApiClass.clear();
|
|
168
232
|
}
|
|
169
233
|
}
|
|
170
234
|
exports.ClientRegistry = ClientRegistry;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorTranslation, ErrorWireForm } from './ErrorTranslation';\nimport { ProtocolError } from './errors';\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /**\n * App-supplied error translations, consulted BEFORE webpieces' built-in error mapping (both\n * directions). Process-global, populated once at startup on the SERVER and in the BROWSER — the\n * same no-DI pattern as {@link ClientRegistry.mappings} above. Consulted in registration order,\n * first match wins, so a later-registered app type AND an override of a built-in status both\n * work. See {@link ErrorTranslation}.\n */\n private static readonly errorTranslations: ErrorTranslation[] = [];\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - typo? svcName must be the CLOUD RUN service name, not your project/module name`,\n );\n }\n return url;\n }\n\n /**\n * Register an app error translation. Consulted BEFORE webpieces' built-in mapping, in\n * registration order (first match wins), so later app types AND overrides of built-ins both\n * work. Call ONCE at startup — on the server AND in the browser — mirroring\n * {@link ClientRegistry.addMapping}. See {@link ErrorTranslation}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addErrorTranslation(translation: ErrorTranslation): void {\n ClientRegistry.errorTranslations.push(translation);\n }\n\n /**\n * exception → wire (SERVER side). The first registered translation that claims `error` wins;\n * `undefined` if none does — the caller then falls through to the generic webpieces mapping.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryTranslateToWire(error: Error): ErrorWireForm | undefined {\n for (const translation of ClientRegistry.errorTranslations) {\n const wire = translation.toWire(error);\n if (wire !== undefined) {\n return wire;\n }\n }\n return undefined;\n }\n\n /**\n * wire → exception (CLIENT side). The first registered translation that claims\n * `(statusCode, protocolError)` wins; `undefined` if none does — the caller then falls through\n * to the generic webpieces switch.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryTranslateFromWire(statusCode: number, protocolError: ProtocolError): Error | undefined {\n for (const translation of ClientRegistry.errorTranslations) {\n const err = translation.fromWire(statusCode, protocolError);\n if (err !== undefined) {\n return err;\n }\n }\n return undefined;\n }\n\n /** Reset mappings, the deriver, AND error translations. For tests, so the process-globals do not leak across specs. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static clear(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.errorTranslations.length = 0;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorTranslation, ErrorWireForm } from './ErrorTranslation';\nimport { ProtocolError } from './errors';\nimport { FailureClassifier } from './FailureClassifier';\nimport { WEBPIECES_DEFAULT_FAILURE_CLASSIFIER } from './WebpiecesDefaultFailureClassifier';\nimport { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /**\n * App-supplied error translations, consulted BEFORE webpieces' built-in error mapping (both\n * directions). Process-global, populated once at startup on the SERVER and in the BROWSER — the\n * same no-DI pattern as {@link ClientRegistry.mappings} above. Consulted in registration order,\n * first match wins, so a later-registered app type AND an override of a built-in status both\n * work. See {@link ErrorTranslation}.\n */\n private static readonly errorTranslations: ErrorTranslation[] = [];\n\n /**\n * The app/company DEFAULT {@link FailureClassifier} — ONE per process, reads {@link ApiMethodInfo.side}\n * so a single strategy covers the server router AND all internal clients. Undefined = use the\n * webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}). Populated once at startup on the\n * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.\n */\n private static appDefaultFailureClassifier: FailureClassifier | undefined;\n\n /**\n * Per-EXTERNAL-client {@link FailureClassifier}s, keyed by {@link ApiMethodInfo.apiClass}\n * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external\n * client overrides the default for its own apiClass only. Internal clients / the server register\n * nothing here and fall through to the default. See {@link ClientRegistry.classifyFailure}.\n */\n private static readonly failureClassifiersByApiClass = new Map<string, FailureClassifier>();\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - typo? svcName must be the CLOUD RUN service name, not your project/module name`,\n );\n }\n return url;\n }\n\n /**\n * Register an app error translation. Consulted BEFORE webpieces' built-in mapping, in\n * registration order (first match wins), so later app types AND overrides of built-ins both\n * work. Call ONCE at startup — on the server AND in the browser — mirroring\n * {@link ClientRegistry.addMapping}. See {@link ErrorTranslation}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addErrorTranslation(translation: ErrorTranslation): void {\n ClientRegistry.errorTranslations.push(translation);\n }\n\n /**\n * exception → wire (SERVER side). The first registered translation that claims `error` wins;\n * `undefined` if none does — the caller then falls through to the generic webpieces mapping.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryTranslateToWire(error: Error): ErrorWireForm | undefined {\n for (const translation of ClientRegistry.errorTranslations) {\n const wire = translation.toWire(error);\n if (wire !== undefined) {\n return wire;\n }\n }\n return undefined;\n }\n\n /**\n * wire → exception (CLIENT side). The first registered translation that claims\n * `(statusCode, protocolError)` wins; `undefined` if none does — the caller then falls through\n * to the generic webpieces switch.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryTranslateFromWire(statusCode: number, protocolError: ProtocolError): Error | undefined {\n for (const translation of ClientRegistry.errorTranslations) {\n const err = translation.fromWire(statusCode, protocolError);\n if (err !== undefined) {\n return err;\n }\n }\n return undefined;\n }\n\n /**\n * Set the app/company DEFAULT failure classifier — ONE per process, covering the server router and\n * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the\n * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier\n * claims. See {@link FailureClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDefaultFailureClassifier(classifier: FailureClassifier): void {\n ClientRegistry.appDefaultFailureClassifier = classifier;\n }\n\n /**\n * Register a per-EXTERNAL-client failure classifier, keyed by {@link ApiMethodInfo.apiClass}\n * (e.g. 'FirestoreAdminClient'). Consulted BEFORE the default tier for calls with that apiClass.\n * Call ONCE at startup. See {@link FailureClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addFailureClassifier(apiClass: string, classifier: FailureClassifier): void {\n ClientRegistry.failureClassifiersByApiClass.set(apiClass, classifier);\n }\n\n /**\n * Is this thrown API-call error a real FAILURE (true) or an expected non-failure (false)?\n * Resolved most-specific-first, and ALWAYS definitive (returns a boolean):\n * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;\n * 2. else the app default classifier, if one was set — unless it defers;\n * 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}), which never defers.\n * A `?? true` backstop keeps an unexpectedly-deferring built-in fail-safe (treat as failure).\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static classifyFailure(error: Error, methodInfo: ApiMethodInfo): boolean {\n const perClient = ClientRegistry.failureClassifiersByApiClass.get(methodInfo.apiClass);\n if (perClient !== undefined) {\n const verdict = perClient.isFailure(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n if (ClientRegistry.appDefaultFailureClassifier !== undefined) {\n const verdict = ClientRegistry.appDefaultFailureClassifier.isFailure(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n return WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(error, methodInfo) ?? true;\n }\n\n /**\n * Reset mappings, the deriver, error translations, AND failure classifiers. For tests, so the\n * process-globals do not leak across specs.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static clear(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.errorTranslations.length = 0;\n ClientRegistry.appDefaultFailureClassifier = undefined;\n ClientRegistry.failureClassifiersByApiClass.clear();\n }\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ApiMethodInfo } from './ApiMethodInfo';
|
|
2
|
+
/**
|
|
3
|
+
* Decides whether a thrown API-call error is a real FAILURE — the process not working, SURFACE it
|
|
4
|
+
* (LogApiCall logs `[API-*-resp-FAIL]`, `jsonPayload.api.result='failure'` — what dashboards/alerts
|
|
5
|
+
* count) — or an EXPECTED non-failure — the process working correctly, a handled condition
|
|
6
|
+
* (`[API-*-resp-OTHER]`, `result='success'`).
|
|
7
|
+
*
|
|
8
|
+
* This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method
|
|
9
|
+
* rather than a bare function — so "find usages" in an IDE lands on every implementation.
|
|
10
|
+
*
|
|
11
|
+
* Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once
|
|
12
|
+
* singleton that owns URL mappings and {@link ErrorTranslation}. TWO tiers, resolved most-specific
|
|
13
|
+
* first (see {@link ClientRegistry.classifyFailure}):
|
|
14
|
+
*
|
|
15
|
+
* 1. `ClientRegistry.setDefaultFailureClassifier(c)` — ONE per app/company. It reads
|
|
16
|
+
* {@link ApiMethodInfo.side}, so a single strategy covers the SERVER router AND all INTERNAL
|
|
17
|
+
* clients (webpieces http client, cloud tasks). Optional: if unset, webpieces uses
|
|
18
|
+
* {@link WebpiecesDefaultFailureClassifier} (server 4xx = non-failure; client non-266 = failure).
|
|
19
|
+
* 2. `ClientRegistry.addFailureClassifier(apiClass, c)` — per EXTERNAL client, keyed by
|
|
20
|
+
* {@link ApiMethodInfo.apiClass} ('FirestoreAdminClient', 'ClaudeApi', 'TwilioApi'). Each external
|
|
21
|
+
* API qualifies errors differently (a Firestore 404 miss, a Twilio 429 retry are EXPECTED, not
|
|
22
|
+
* failures), so it overrides the default for that apiClass only.
|
|
23
|
+
*
|
|
24
|
+
* Internal clients and the server register NOTHING — the ABSENCE of an apiClass entry is the signal;
|
|
25
|
+
* they fall through to tier 1. An external client that forgets to register is FAIL-SAFE: it also
|
|
26
|
+
* falls to tier 1 (client side ⇒ every non-266 = failure) until it opts in.
|
|
27
|
+
*/
|
|
28
|
+
export interface FailureClassifier {
|
|
29
|
+
/**
|
|
30
|
+
* @param error - the already-normalized thrown error (callers pass `toError(err)`)
|
|
31
|
+
* @param methodInfo - the call identity; `side` distinguishes server/client, `apiClass` the client
|
|
32
|
+
* @returns `true` = real failure; `false` = expected/non-failure; `undefined` = DEFER
|
|
33
|
+
* (a per-apiClass classifier defers to the app default; the app default defers to the
|
|
34
|
+
* webpieces built-in). Deferring is what makes classifiers additive AND override-capable.
|
|
35
|
+
*/
|
|
36
|
+
isFailure(error: Error, methodInfo: ApiMethodInfo): boolean | undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the
|
|
40
|
+
* {@link FailureClassifier} to apply for it. A data-only structure, so it is a class (not an inline
|
|
41
|
+
* object literal), per CLAUDE.md — lets startup wiring carry a list of these and hand each to
|
|
42
|
+
* {@link ClientRegistry.addFailureClassifier}.
|
|
43
|
+
*/
|
|
44
|
+
export declare class KeyedFailureClassifier {
|
|
45
|
+
readonly apiClass: string;
|
|
46
|
+
readonly classifier: FailureClassifier;
|
|
47
|
+
constructor(apiClass: string, classifier: FailureClassifier);
|
|
48
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyedFailureClassifier = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the
|
|
6
|
+
* {@link FailureClassifier} to apply for it. A data-only structure, so it is a class (not an inline
|
|
7
|
+
* object literal), per CLAUDE.md — lets startup wiring carry a list of these and hand each to
|
|
8
|
+
* {@link ClientRegistry.addFailureClassifier}.
|
|
9
|
+
*/
|
|
10
|
+
class KeyedFailureClassifier {
|
|
11
|
+
apiClass;
|
|
12
|
+
classifier;
|
|
13
|
+
constructor(apiClass, classifier) {
|
|
14
|
+
this.apiClass = apiClass;
|
|
15
|
+
this.classifier = classifier;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.KeyedFailureClassifier = KeyedFailureClassifier;
|
|
19
|
+
//# sourceMappingURL=FailureClassifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FailureClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/FailureClassifier.ts"],"names":[],"mappings":";;;AAuCA;;;;;GAKG;AACH,MAAa,sBAAsB;IAEX;IACA;IAFpB,YACoB,QAAgB,EAChB,UAA6B;QAD7B,aAAQ,GAAR,QAAQ,CAAQ;QAChB,eAAU,GAAV,UAAU,CAAmB;IAC9C,CAAC;CACP;AALD,wDAKC","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * Decides whether a thrown API-call error is a real FAILURE — the process not working, SURFACE it\n * (LogApiCall logs `[API-*-resp-FAIL]`, `jsonPayload.api.result='failure'` — what dashboards/alerts\n * count) — or an EXPECTED non-failure — the process working correctly, a handled condition\n * (`[API-*-resp-OTHER]`, `result='success'`).\n *\n * This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method\n * rather than a bare function — so \"find usages\" in an IDE lands on every implementation.\n *\n * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once\n * singleton that owns URL mappings and {@link ErrorTranslation}. TWO tiers, resolved most-specific\n * first (see {@link ClientRegistry.classifyFailure}):\n *\n * 1. `ClientRegistry.setDefaultFailureClassifier(c)` — ONE per app/company. It reads\n * {@link ApiMethodInfo.side}, so a single strategy covers the SERVER router AND all INTERNAL\n * clients (webpieces http client, cloud tasks). Optional: if unset, webpieces uses\n * {@link WebpiecesDefaultFailureClassifier} (server 4xx = non-failure; client non-266 = failure).\n * 2. `ClientRegistry.addFailureClassifier(apiClass, c)` — per EXTERNAL client, keyed by\n * {@link ApiMethodInfo.apiClass} ('FirestoreAdminClient', 'ClaudeApi', 'TwilioApi'). Each external\n * API qualifies errors differently (a Firestore 404 miss, a Twilio 429 retry are EXPECTED, not\n * failures), so it overrides the default for that apiClass only.\n *\n * Internal clients and the server register NOTHING — the ABSENCE of an apiClass entry is the signal;\n * they fall through to tier 1. An external client that forgets to register is FAIL-SAFE: it also\n * falls to tier 1 (client side ⇒ every non-266 = failure) until it opts in.\n */\nexport interface FailureClassifier {\n /**\n * @param error - the already-normalized thrown error (callers pass `toError(err)`)\n * @param methodInfo - the call identity; `side` distinguishes server/client, `apiClass` the client\n * @returns `true` = real failure; `false` = expected/non-failure; `undefined` = DEFER\n * (a per-apiClass classifier defers to the app default; the app default defers to the\n * webpieces built-in). Deferring is what makes classifiers additive AND override-capable.\n */\n isFailure(error: Error, methodInfo: ApiMethodInfo): boolean | undefined;\n}\n\n/**\n * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the\n * {@link FailureClassifier} to apply for it. A data-only structure, so it is a class (not an inline\n * object literal), per CLAUDE.md — lets startup wiring carry a list of these and hand each to\n * {@link ClientRegistry.addFailureClassifier}.\n */\nexport class KeyedFailureClassifier {\n constructor(\n public readonly apiClass: string,\n public readonly classifier: FailureClassifier,\n ) {}\n}\n"]}
|
package/src/http/LogApiCall.d.ts
CHANGED
|
@@ -60,28 +60,12 @@ export declare class LogApiCallImpl {
|
|
|
60
60
|
* Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api
|
|
61
61
|
* result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?
|
|
62
62
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* both re-couples to HTTP AND would bury 408.
|
|
70
|
-
*
|
|
71
|
-
* SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:
|
|
72
|
-
* - HttpBadRequestError (400) "your request is malformed"
|
|
73
|
-
* - HttpUnauthorizedError (401) "you're not authenticated"
|
|
74
|
-
* - HttpForbiddenError (403) "authenticated, but not allowed"
|
|
75
|
-
* - HttpNotFoundError (404) "wrong url / no such entity" (EndpointNotFoundError is a subclass)
|
|
76
|
-
* → the server is fine, the caller erred → result:'success', logged OTHER.
|
|
77
|
-
*
|
|
78
|
-
* SERVER — something may actually be WRONG, so SURFACE it (result:'failure', logged FAIL):
|
|
79
|
-
* - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately
|
|
80
|
-
* absent from the list below so it counts as a failure.
|
|
81
|
-
* - 500 / 502 / 504 / 598, and any non-Http Error: real failures.
|
|
82
|
-
*
|
|
83
|
-
* HttpUserError (266): ALWAYS a non-failure, server OR client — an expected "user made a mistake" signal.
|
|
84
|
-
* CLIENT: receiving ANY error except 266 means the outbound call FAILED → result:'failure'.
|
|
63
|
+
* BACK-COMPAT SHIM: the canonical logic now lives in {@link WebpiecesDefaultFailureClassifier}
|
|
64
|
+
* (the webpieces built-in tier), and the LIVE classification path is
|
|
65
|
+
* {@link ClientRegistry.classifyFailure} (per-apiClass → app default → built-in). This method
|
|
66
|
+
* delegates to the built-in so existing callers/tests keep the exact old behavior; it does NOT
|
|
67
|
+
* consult registered classifiers. `apiClass`/`methodName` are irrelevant to the built-in (it reads
|
|
68
|
+
* only `side`), hence the empty strings.
|
|
85
69
|
*
|
|
86
70
|
* @param error - The already-normalized error (callers pass toError(err), never a raw catch value)
|
|
87
71
|
* @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call
|
package/src/http/LogApiCall.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LogApiCall = exports.LogApiCallImpl = void 0;
|
|
4
|
-
const errors_1 = require("./errors");
|
|
5
4
|
const errorUtils_1 = require("../lib/errorUtils");
|
|
6
5
|
const LogManager_1 = require("../logging/LogManager");
|
|
7
6
|
const ApiCallInfo_1 = require("./ApiCallInfo");
|
|
7
|
+
const ApiMethodInfo_1 = require("./ApiMethodInfo");
|
|
8
8
|
const ApiCallContext_1 = require("./ApiCallContext");
|
|
9
9
|
const WebpiecesCoreHeaders_1 = require("./WebpiecesCoreHeaders");
|
|
10
10
|
const ApiCallLogName_1 = require("./ApiCallLogName");
|
|
11
|
+
const ClientRegistry_1 = require("./ClientRegistry");
|
|
12
|
+
const WebpiecesDefaultFailureClassifier_1 = require("./WebpiecesDefaultFailureClassifier");
|
|
11
13
|
// The console backends special-case THIS logger name into a self-describing [API.{side}.{phase}]
|
|
12
14
|
// bracket (see ApiCallLogName) — so the name here and the name they match are the one constant.
|
|
13
15
|
const log = LogManager_1.LogManager.getLogger(ApiCallLogName_1.LOG_API_CALL_LOGGER_NAME);
|
|
@@ -114,9 +116,11 @@ class LogApiCallImpl {
|
|
|
114
116
|
const side = methodInfo.side;
|
|
115
117
|
const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;
|
|
116
118
|
const errorType = error.constructor.name;
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
119
|
+
// Pluggable classification (ClientRegistry): a per-apiClass EXTERNAL-client classifier wins,
|
|
120
|
+
// else the app default, else the webpieces built-in — which is side-dependent (a 4xx the SERVER
|
|
121
|
+
// raised is a handled non-failure; the same 4xx a CLIENT receives means its call FAILED; 266 is
|
|
122
|
+
// never a failure either side). `isUser` = "treat as non-failure (OTHER / result:'success')".
|
|
123
|
+
const isUser = !ClientRegistry_1.ClientRegistry.classifyFailure(error, methodInfo);
|
|
120
124
|
stamp(new ApiCallInfo_1.ApiCallInfo(methodInfo, 'response', isUser ? 'success' : 'failure', durationMs, requestSize), () => isUser
|
|
121
125
|
? log.warn(`[API-${side}-resp-OTHER] ${id} errorType=${errorType}`)
|
|
122
126
|
: log.error(`[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${error.message}`));
|
|
@@ -136,48 +140,19 @@ class LogApiCallImpl {
|
|
|
136
140
|
* Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api
|
|
137
141
|
* result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?
|
|
138
142
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* both re-couples to HTTP AND would bury 408.
|
|
146
|
-
*
|
|
147
|
-
* SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:
|
|
148
|
-
* - HttpBadRequestError (400) "your request is malformed"
|
|
149
|
-
* - HttpUnauthorizedError (401) "you're not authenticated"
|
|
150
|
-
* - HttpForbiddenError (403) "authenticated, but not allowed"
|
|
151
|
-
* - HttpNotFoundError (404) "wrong url / no such entity" (EndpointNotFoundError is a subclass)
|
|
152
|
-
* → the server is fine, the caller erred → result:'success', logged OTHER.
|
|
153
|
-
*
|
|
154
|
-
* SERVER — something may actually be WRONG, so SURFACE it (result:'failure', logged FAIL):
|
|
155
|
-
* - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately
|
|
156
|
-
* absent from the list below so it counts as a failure.
|
|
157
|
-
* - 500 / 502 / 504 / 598, and any non-Http Error: real failures.
|
|
158
|
-
*
|
|
159
|
-
* HttpUserError (266): ALWAYS a non-failure, server OR client — an expected "user made a mistake" signal.
|
|
160
|
-
* CLIENT: receiving ANY error except 266 means the outbound call FAILED → result:'failure'.
|
|
143
|
+
* BACK-COMPAT SHIM: the canonical logic now lives in {@link WebpiecesDefaultFailureClassifier}
|
|
144
|
+
* (the webpieces built-in tier), and the LIVE classification path is
|
|
145
|
+
* {@link ClientRegistry.classifyFailure} (per-apiClass → app default → built-in). This method
|
|
146
|
+
* delegates to the built-in so existing callers/tests keep the exact old behavior; it does NOT
|
|
147
|
+
* consult registered classifiers. `apiClass`/`methodName` are irrelevant to the built-in (it reads
|
|
148
|
+
* only `side`), hence the empty strings.
|
|
161
149
|
*
|
|
162
150
|
* @param error - The already-normalized error (callers pass toError(err), never a raw catch value)
|
|
163
151
|
* @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call
|
|
164
152
|
* @returns true if this should be treated as a non-failure (OTHER / result:'success')
|
|
165
153
|
*/
|
|
166
154
|
isUserError(error, server) {
|
|
167
|
-
|
|
168
|
-
if (error instanceof errors_1.HttpUserError) {
|
|
169
|
-
return true;
|
|
170
|
-
}
|
|
171
|
-
// A client that RECEIVED any error (except the 266 above) made a failed call.
|
|
172
|
-
if (!server) {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
// SERVER: only a healthy rejection of the caller's mistake is a non-failure. Note 408
|
|
176
|
-
// (HttpTimeoutError) is intentionally NOT here — the client may never have seen the response.
|
|
177
|
-
return (error instanceof errors_1.HttpBadRequestError ||
|
|
178
|
-
error instanceof errors_1.HttpUnauthorizedError ||
|
|
179
|
-
error instanceof errors_1.HttpForbiddenError ||
|
|
180
|
-
error instanceof errors_1.HttpNotFoundError);
|
|
155
|
+
return !WebpiecesDefaultFailureClassifier_1.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(error, new ApiMethodInfo_1.ApiMethodInfo(server ? 'server' : 'client', '', ''));
|
|
181
156
|
}
|
|
182
157
|
}
|
|
183
158
|
exports.LogApiCallImpl = LogApiCallImpl;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LogApiCall.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/LogApiCall.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpNotFoundError,\n HttpUserError,\n} from './errors';\nimport {toError} from \"../lib/errorUtils\";\nimport {LogManager} from \"../logging/LogManager\";\nimport {ApiCallInfo} from \"./ApiCallInfo\";\nimport {ApiMethodInfo} from \"./ApiMethodInfo\";\nimport {ApiCallContext, ApiCallContextHolder} from \"./ApiCallContext\";\nimport {WebpiecesCoreHeaders} from \"./WebpiecesCoreHeaders\";\nimport {LOG_API_CALL_LOGGER_NAME} from \"./ApiCallLogName\";\n\n// The console backends special-case THIS logger name into a self-describing [API.{side}.{phase}]\n// bracket (see ApiCallLogName) — so the name here and the name they match are the one constant.\nconst log = LogManager.getLogger(LOG_API_CALL_LOGGER_NAME);\n\n/**\n * LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and\n * client-side (ProxyClient) for one consistent logging shape across the framework.\n *\n * TWO things happen around each call:\n * 1. Text lines are emitted (the human-readable `[API-...]` patterns below).\n * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the\n * {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the\n * req/resp lines) inherits a filterable `api` object — surfacing in GCP as\n * `jsonPayload.api.{method.{side,apiClass,methodName,controllerName},type,result}`.\n *\n * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →\n * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).\n * It stamps through the {@link ApiCallContext} seam instead: `setupRuntime` installs a\n * RequestContext-backed impl on a Node server, and `ClientHttpBrowserFactory` a module-global impl in a\n * browser. If neither ran, {@link ApiCallContextHolder.get} throws (loud misconfiguration).\n *\n * Singleton, mirroring `RequestContext`: use the exported {@link LogApiCall} constant, not `new`.\n *\n * Logging format patterns:\n * - [API-{side}-req] ClassName.methodName request={...}\n * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}\n * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} (user errors)\n * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (server errors)\n */\nexport class LogApiCallImpl {\n\n /**\n * Execute an API call with logging + `api` context-tagging around it.\n *\n * @param methodInfo - The transport-neutral call identity (side, apiClass, methodName,\n * controllerName?). `apiClass` is what matches a client call to its server handler in the logs.\n * @param requestDto - The request DTO (external multi-param callers synthesize a small object)\n * @param method - The method to execute\n *\n * Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,\n * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only\n * for the SYNCHRONOUS span of each log line: set → log → remove. Because the tag is never held across\n * `await method(...)`, a concurrent browser call (single-threaded, one global slot) can never clobber\n * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is\n * exactly what the GCP filters (`jsonPayload.api.*`) want.\n */\n public async execute(\n methodInfo: ApiMethodInfo,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches ProxyClient)\n requestDto: any,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n method: (dto: any) => Promise<any>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n ): Promise<any> {\n const ctx = this.activeContext();\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n // set → emit → remove, as ONE synchronous span: the tag is live only while the logger reads it,\n // never across an await, so a single browser global slot can never be clobbered by a concurrent call.\n const stamp = (info: ApiCallInfo, emit: () => void): void => {\n ctx.set(key, info);\n emit();\n ctx.remove(key);\n };\n\n // Stringify ONCE and reuse for both the log text and the size — a second JSON.stringify of a\n // large DTO purely to measure it would double the cost of the thing we are measuring.\n const requestBody = JSON.stringify(requestDto);\n const requestSize = this.byteSize(requestBody);\n // Declared out here so the catch below can read it too. Reassigned just before the call, so\n // the number times ONLY the call and not our own request-logging.\n let startMs = Date.now();\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller\n try {\n stamp(new ApiCallInfo(methodInfo, 'request', undefined, undefined, requestSize), () =>\n log.info(`[API-${side}-req] ${id} request=${requestBody}`));\n\n if(!requestDto)\n throw new Error(`Request cannot be null and was from ${id}`);\n\n startMs = Date.now();\n const response = await method(requestDto);\n const durationMs = Date.now() - startMs;\n\n const responseBody = JSON.stringify(response);\n stamp(\n new ApiCallInfo(\n methodInfo, 'response', 'success', durationMs, requestSize, this.byteSize(responseBody),\n ),\n () => log.info(`[API-${side}-resp-SUCCESS] ${id} response=${responseBody}`));\n\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n // Duration comes off the SAME start as the success path, so a slow failure (a timeout, a\n // hung dependency) reports its real cost rather than nothing.\n this.logFailure(error, methodInfo, Date.now() - startMs, requestSize, stamp);\n throw error;\n }\n }\n\n /**\n * The ApiCallContext to stamp into. Throws if none was installed at startup, or if there is no\n * active scope — loud misconfiguration, because an api call with nowhere to tag is a bug.\n */\n private activeContext(): ApiCallContext {\n const ctx = ApiCallContextHolder.get();\n if (!ctx.isActive()) {\n throw new Error(\n 'LogApiCall requires an ACTIVE ApiCallContext. On a Node server, run inside the ' +\n 'RequestContext.run(...) a server filter opens; in a browser, build ClientHttpBrowserFactory first.',\n );\n }\n return ctx;\n }\n\n /**\n * Tag + log a thrown call. There is no responseSize — a throw produced no response body to measure.\n */\n private logFailure(\n error: Error,\n methodInfo: ApiMethodInfo,\n durationMs: number,\n requestSize: number | undefined,\n stamp: (info: ApiCallInfo, emit: () => void) => void,\n ): void {\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n const errorType = error.constructor.name;\n // Side-dependent: a 4xx the SERVER raised is a handled non-failure (OTHER); the same 4xx a\n // CLIENT receives means its call FAILED. HttpUserError (266) is never a failure, either side.\n const isUser = this.isUserError(error, side === 'server');\n\n stamp(\n new ApiCallInfo(methodInfo, 'response', isUser ? 'success' : 'failure', durationMs, requestSize),\n () => isUser\n ? log.warn(`[API-${side}-resp-OTHER] ${id} errorType=${errorType}`)\n : log.error(`[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${error.message}`));\n }\n\n /**\n * UTF-8 byte size of an already-serialized body. TextEncoder, not Buffer: LogApiCall runs in the\n * browser bundle. Undefined in, undefined out — a `Promise<void>` method has no body to measure,\n * and a 0 there would be a lie (JSON.stringify(undefined) returns undefined, not '').\n */\n private byteSize(serialized: string | undefined): number | undefined {\n if (serialized === undefined) {\n return undefined;\n }\n return new TextEncoder().encode(serialized).length;\n }\n\n /**\n * Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api\n * result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?\n *\n * The question is \"are things WORKING?\", NOT \"was it an HTTP 4xx vs 5xx\" — and the two are not the\n * same (see 408 below). Classification is by portable Error TYPE, never by transport: LogApiCall runs\n * deep in the stack (in-process calls, pubsub/queue handlers, HTTP — one code path) and only ever\n * sees a thrown Error. The Http* classes are poorly named for that (the `Http` prefix is historical),\n * but they ARE portable error types that travel with the throw anywhere, so matching the TYPE works\n * with or without any HTTP in the picture. Do NOT swap this for an `error.code` 4xx range check — that\n * both re-couples to HTTP AND would bury 408.\n *\n * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:\n * - HttpBadRequestError (400) \"your request is malformed\"\n * - HttpUnauthorizedError (401) \"you're not authenticated\"\n * - HttpForbiddenError (403) \"authenticated, but not allowed\"\n * - HttpNotFoundError (404) \"wrong url / no such entity\" (EndpointNotFoundError is a subclass)\n * → the server is fine, the caller erred → result:'success', logged OTHER.\n *\n * SERVER — something may actually be WRONG, so SURFACE it (result:'failure', logged FAIL):\n * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately\n * absent from the list below so it counts as a failure.\n * - 500 / 502 / 504 / 598, and any non-Http Error: real failures.\n *\n * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected \"user made a mistake\" signal.\n * CLIENT: receiving ANY error except 266 means the outbound call FAILED → result:'failure'.\n *\n * @param error - The already-normalized error (callers pass toError(err), never a raw catch value)\n * @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call\n * @returns true if this should be treated as a non-failure (OTHER / result:'success')\n */\n isUserError(error: Error, server: boolean): boolean {\n // 266 is the one error that is never a failure, on either side.\n if (error instanceof HttpUserError) {\n return true;\n }\n // A client that RECEIVED any error (except the 266 above) made a failed call.\n if (!server) {\n return false;\n }\n // SERVER: only a healthy rejection of the caller's mistake is a non-failure. Note 408\n // (HttpTimeoutError) is intentionally NOT here — the client may never have seen the response.\n return (\n error instanceof HttpBadRequestError ||\n error instanceof HttpUnauthorizedError ||\n error instanceof HttpForbiddenError ||\n error instanceof HttpNotFoundError\n );\n }\n}\n\n/**\n * The process-wide {@link LogApiCallImpl} singleton — mirrors the `RequestContext` export pattern.\n * Callers use `LogApiCall.execute(...)`, never `new`.\n */\nexport const LogApiCall = new LogApiCallImpl();\n"]}
|
|
1
|
+
{"version":3,"file":"LogApiCall.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/LogApiCall.ts"],"names":[],"mappings":"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","sourcesContent":["import {toError} from \"../lib/errorUtils\";\nimport {LogManager} from \"../logging/LogManager\";\nimport {ApiCallInfo} from \"./ApiCallInfo\";\nimport {ApiMethodInfo} from \"./ApiMethodInfo\";\nimport {ApiCallContext, ApiCallContextHolder} from \"./ApiCallContext\";\nimport {WebpiecesCoreHeaders} from \"./WebpiecesCoreHeaders\";\nimport {LOG_API_CALL_LOGGER_NAME} from \"./ApiCallLogName\";\nimport {ClientRegistry} from \"./ClientRegistry\";\nimport {WEBPIECES_DEFAULT_FAILURE_CLASSIFIER} from \"./WebpiecesDefaultFailureClassifier\";\n\n// The console backends special-case THIS logger name into a self-describing [API.{side}.{phase}]\n// bracket (see ApiCallLogName) — so the name here and the name they match are the one constant.\nconst log = LogManager.getLogger(LOG_API_CALL_LOGGER_NAME);\n\n/**\n * LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and\n * client-side (ProxyClient) for one consistent logging shape across the framework.\n *\n * TWO things happen around each call:\n * 1. Text lines are emitted (the human-readable `[API-...]` patterns below).\n * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the\n * {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the\n * req/resp lines) inherits a filterable `api` object — surfacing in GCP as\n * `jsonPayload.api.{method.{side,apiClass,methodName,controllerName},type,result}`.\n *\n * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →\n * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).\n * It stamps through the {@link ApiCallContext} seam instead: `setupRuntime` installs a\n * RequestContext-backed impl on a Node server, and `ClientHttpBrowserFactory` a module-global impl in a\n * browser. If neither ran, {@link ApiCallContextHolder.get} throws (loud misconfiguration).\n *\n * Singleton, mirroring `RequestContext`: use the exported {@link LogApiCall} constant, not `new`.\n *\n * Logging format patterns:\n * - [API-{side}-req] ClassName.methodName request={...}\n * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}\n * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} (user errors)\n * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (server errors)\n */\nexport class LogApiCallImpl {\n\n /**\n * Execute an API call with logging + `api` context-tagging around it.\n *\n * @param methodInfo - The transport-neutral call identity (side, apiClass, methodName,\n * controllerName?). `apiClass` is what matches a client call to its server handler in the logs.\n * @param requestDto - The request DTO (external multi-param callers synthesize a small object)\n * @param method - The method to execute\n *\n * Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,\n * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only\n * for the SYNCHRONOUS span of each log line: set → log → remove. Because the tag is never held across\n * `await method(...)`, a concurrent browser call (single-threaded, one global slot) can never clobber\n * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is\n * exactly what the GCP filters (`jsonPayload.api.*`) want.\n */\n public async execute(\n methodInfo: ApiMethodInfo,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches ProxyClient)\n requestDto: any,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n method: (dto: any) => Promise<any>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n ): Promise<any> {\n const ctx = this.activeContext();\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n // set → emit → remove, as ONE synchronous span: the tag is live only while the logger reads it,\n // never across an await, so a single browser global slot can never be clobbered by a concurrent call.\n const stamp = (info: ApiCallInfo, emit: () => void): void => {\n ctx.set(key, info);\n emit();\n ctx.remove(key);\n };\n\n // Stringify ONCE and reuse for both the log text and the size — a second JSON.stringify of a\n // large DTO purely to measure it would double the cost of the thing we are measuring.\n const requestBody = JSON.stringify(requestDto);\n const requestSize = this.byteSize(requestBody);\n // Declared out here so the catch below can read it too. Reassigned just before the call, so\n // the number times ONLY the call and not our own request-logging.\n let startMs = Date.now();\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller\n try {\n stamp(new ApiCallInfo(methodInfo, 'request', undefined, undefined, requestSize), () =>\n log.info(`[API-${side}-req] ${id} request=${requestBody}`));\n\n if(!requestDto)\n throw new Error(`Request cannot be null and was from ${id}`);\n\n startMs = Date.now();\n const response = await method(requestDto);\n const durationMs = Date.now() - startMs;\n\n const responseBody = JSON.stringify(response);\n stamp(\n new ApiCallInfo(\n methodInfo, 'response', 'success', durationMs, requestSize, this.byteSize(responseBody),\n ),\n () => log.info(`[API-${side}-resp-SUCCESS] ${id} response=${responseBody}`));\n\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n // Duration comes off the SAME start as the success path, so a slow failure (a timeout, a\n // hung dependency) reports its real cost rather than nothing.\n this.logFailure(error, methodInfo, Date.now() - startMs, requestSize, stamp);\n throw error;\n }\n }\n\n /**\n * The ApiCallContext to stamp into. Throws if none was installed at startup, or if there is no\n * active scope — loud misconfiguration, because an api call with nowhere to tag is a bug.\n */\n private activeContext(): ApiCallContext {\n const ctx = ApiCallContextHolder.get();\n if (!ctx.isActive()) {\n throw new Error(\n 'LogApiCall requires an ACTIVE ApiCallContext. On a Node server, run inside the ' +\n 'RequestContext.run(...) a server filter opens; in a browser, build ClientHttpBrowserFactory first.',\n );\n }\n return ctx;\n }\n\n /**\n * Tag + log a thrown call. There is no responseSize — a throw produced no response body to measure.\n */\n private logFailure(\n error: Error,\n methodInfo: ApiMethodInfo,\n durationMs: number,\n requestSize: number | undefined,\n stamp: (info: ApiCallInfo, emit: () => void) => void,\n ): void {\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n const errorType = error.constructor.name;\n // Pluggable classification (ClientRegistry): a per-apiClass EXTERNAL-client classifier wins,\n // else the app default, else the webpieces built-in — which is side-dependent (a 4xx the SERVER\n // raised is a handled non-failure; the same 4xx a CLIENT receives means its call FAILED; 266 is\n // never a failure either side). `isUser` = \"treat as non-failure (OTHER / result:'success')\".\n const isUser = !ClientRegistry.classifyFailure(error, methodInfo);\n\n stamp(\n new ApiCallInfo(methodInfo, 'response', isUser ? 'success' : 'failure', durationMs, requestSize),\n () => isUser\n ? log.warn(`[API-${side}-resp-OTHER] ${id} errorType=${errorType}`)\n : log.error(`[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${error.message}`));\n }\n\n /**\n * UTF-8 byte size of an already-serialized body. TextEncoder, not Buffer: LogApiCall runs in the\n * browser bundle. Undefined in, undefined out — a `Promise<void>` method has no body to measure,\n * and a 0 there would be a lie (JSON.stringify(undefined) returns undefined, not '').\n */\n private byteSize(serialized: string | undefined): number | undefined {\n if (serialized === undefined) {\n return undefined;\n }\n return new TextEncoder().encode(serialized).length;\n }\n\n /**\n * Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api\n * result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?\n *\n * BACK-COMPAT SHIM: the canonical logic now lives in {@link WebpiecesDefaultFailureClassifier}\n * (the webpieces built-in tier), and the LIVE classification path is\n * {@link ClientRegistry.classifyFailure} (per-apiClass → app default → built-in). This method\n * delegates to the built-in so existing callers/tests keep the exact old behavior; it does NOT\n * consult registered classifiers. `apiClass`/`methodName` are irrelevant to the built-in (it reads\n * only `side`), hence the empty strings.\n *\n * @param error - The already-normalized error (callers pass toError(err), never a raw catch value)\n * @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call\n * @returns true if this should be treated as a non-failure (OTHER / result:'success')\n */\n isUserError(error: Error, server: boolean): boolean {\n return !WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(\n error,\n new ApiMethodInfo(server ? 'server' : 'client', '', ''),\n );\n }\n}\n\n/**\n * The process-wide {@link LogApiCallImpl} singleton — mirrors the `RequestContext` export pattern.\n * Callers use `LogApiCall.execute(...)`, never `new`.\n */\nexport const LogApiCall = new LogApiCallImpl();\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ApiMethodInfo } from './ApiMethodInfo';
|
|
2
|
+
import { FailureClassifier } from './FailureClassifier';
|
|
3
|
+
/**
|
|
4
|
+
* The webpieces built-in {@link FailureClassifier} — the terminal tier, used when no app default and
|
|
5
|
+
* no per-apiClass classifier claims an error. It is the SINGLE SOURCE OF TRUTH for the historical
|
|
6
|
+
* `LogApiCall.isUserError` behavior (which now delegates here), so the classification lives in one
|
|
7
|
+
* place.
|
|
8
|
+
*
|
|
9
|
+
* The question is "are things WORKING?", NOT "was it an HTTP 4xx vs 5xx" — the two differ (see 408).
|
|
10
|
+
* Classification is by portable Error TYPE, never by transport: this runs over in-process calls,
|
|
11
|
+
* pubsub/queue handlers, and HTTP through one code path and only ever sees a thrown Error. The Http*
|
|
12
|
+
* classes are portable error types that travel with the throw anywhere, so matching the TYPE works
|
|
13
|
+
* with or without any HTTP in the picture.
|
|
14
|
+
*
|
|
15
|
+
* SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:
|
|
16
|
+
* - HttpBadRequestError (400), HttpUnauthorizedError (401), HttpForbiddenError (403),
|
|
17
|
+
* HttpNotFoundError (404) → the server is fine, the caller erred → NON-failure.
|
|
18
|
+
* SERVER — something may actually be WRONG, so SURFACE it (failure):
|
|
19
|
+
* - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately
|
|
20
|
+
* absent below, so it counts as a failure. 500/502/504/598 and any non-Http Error: real failures.
|
|
21
|
+
*
|
|
22
|
+
* HttpUserError (266): ALWAYS a non-failure, server OR client — an expected "user made a mistake".
|
|
23
|
+
* CLIENT: receiving ANY error except 266 means the outbound call FAILED → failure.
|
|
24
|
+
*/
|
|
25
|
+
export declare class WebpiecesDefaultFailureClassifier implements FailureClassifier {
|
|
26
|
+
/**
|
|
27
|
+
* Terminal tier — NEVER returns undefined (a definitive verdict is required so classification
|
|
28
|
+
* always resolves to a boolean).
|
|
29
|
+
*
|
|
30
|
+
* @returns true = failure, false = expected/non-failure
|
|
31
|
+
*/
|
|
32
|
+
isFailure(error: Error, methodInfo: ApiMethodInfo): boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Process-wide built-in instance — stateless, so one shared instance is enough. */
|
|
35
|
+
export declare const WEBPIECES_DEFAULT_FAILURE_CLASSIFIER: WebpiecesDefaultFailureClassifier;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER = exports.WebpiecesDefaultFailureClassifier = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
/**
|
|
6
|
+
* The webpieces built-in {@link FailureClassifier} — the terminal tier, used when no app default and
|
|
7
|
+
* no per-apiClass classifier claims an error. It is the SINGLE SOURCE OF TRUTH for the historical
|
|
8
|
+
* `LogApiCall.isUserError` behavior (which now delegates here), so the classification lives in one
|
|
9
|
+
* place.
|
|
10
|
+
*
|
|
11
|
+
* The question is "are things WORKING?", NOT "was it an HTTP 4xx vs 5xx" — the two differ (see 408).
|
|
12
|
+
* Classification is by portable Error TYPE, never by transport: this runs over in-process calls,
|
|
13
|
+
* pubsub/queue handlers, and HTTP through one code path and only ever sees a thrown Error. The Http*
|
|
14
|
+
* classes are portable error types that travel with the throw anywhere, so matching the TYPE works
|
|
15
|
+
* with or without any HTTP in the picture.
|
|
16
|
+
*
|
|
17
|
+
* SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:
|
|
18
|
+
* - HttpBadRequestError (400), HttpUnauthorizedError (401), HttpForbiddenError (403),
|
|
19
|
+
* HttpNotFoundError (404) → the server is fine, the caller erred → NON-failure.
|
|
20
|
+
* SERVER — something may actually be WRONG, so SURFACE it (failure):
|
|
21
|
+
* - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately
|
|
22
|
+
* absent below, so it counts as a failure. 500/502/504/598 and any non-Http Error: real failures.
|
|
23
|
+
*
|
|
24
|
+
* HttpUserError (266): ALWAYS a non-failure, server OR client — an expected "user made a mistake".
|
|
25
|
+
* CLIENT: receiving ANY error except 266 means the outbound call FAILED → failure.
|
|
26
|
+
*/
|
|
27
|
+
class WebpiecesDefaultFailureClassifier {
|
|
28
|
+
/**
|
|
29
|
+
* Terminal tier — NEVER returns undefined (a definitive verdict is required so classification
|
|
30
|
+
* always resolves to a boolean).
|
|
31
|
+
*
|
|
32
|
+
* @returns true = failure, false = expected/non-failure
|
|
33
|
+
*/
|
|
34
|
+
isFailure(error, methodInfo) {
|
|
35
|
+
// 266 is the one error that is never a failure, on either side.
|
|
36
|
+
if (error instanceof errors_1.HttpUserError) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
// A client that RECEIVED any error (except the 266 above) made a failed call.
|
|
40
|
+
if (methodInfo.side !== 'server') {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
// SERVER: only a healthy rejection of the caller's mistake is a non-failure. Note 408
|
|
44
|
+
// (HttpTimeoutError) is intentionally NOT here — the client may never have seen the response.
|
|
45
|
+
const healthyRejection = error instanceof errors_1.HttpBadRequestError ||
|
|
46
|
+
error instanceof errors_1.HttpUnauthorizedError ||
|
|
47
|
+
error instanceof errors_1.HttpForbiddenError ||
|
|
48
|
+
error instanceof errors_1.HttpNotFoundError;
|
|
49
|
+
return !healthyRejection;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.WebpiecesDefaultFailureClassifier = WebpiecesDefaultFailureClassifier;
|
|
53
|
+
/** Process-wide built-in instance — stateless, so one shared instance is enough. */
|
|
54
|
+
exports.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER = new WebpiecesDefaultFailureClassifier();
|
|
55
|
+
//# sourceMappingURL=WebpiecesDefaultFailureClassifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebpiecesDefaultFailureClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultFailureClassifier.ts"],"names":[],"mappings":";;;AAAA,qCAMkB;AAIlB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAa,iCAAiC;IAC1C;;;;;OAKG;IACH,SAAS,CAAC,KAAY,EAAE,UAAyB;QAC7C,gEAAgE;QAChE,IAAI,KAAK,YAAY,sBAAa,EAAE,CAAC;YACjC,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,8EAA8E;QAC9E,IAAI,UAAU,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC/B,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,sFAAsF;QACtF,8FAA8F;QAC9F,MAAM,gBAAgB,GAClB,KAAK,YAAY,4BAAmB;YACpC,KAAK,YAAY,8BAAqB;YACtC,KAAK,YAAY,2BAAkB;YACnC,KAAK,YAAY,0BAAiB,CAAC;QACvC,OAAO,CAAC,gBAAgB,CAAC;IAC7B,CAAC;CACJ;AAzBD,8EAyBC;AAED,oFAAoF;AACvE,QAAA,oCAAoC,GAAG,IAAI,iCAAiC,EAAE,CAAC","sourcesContent":["import {\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpNotFoundError,\n HttpUserError,\n} from './errors';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { FailureClassifier } from './FailureClassifier';\n\n/**\n * The webpieces built-in {@link FailureClassifier} — the terminal tier, used when no app default and\n * no per-apiClass classifier claims an error. It is the SINGLE SOURCE OF TRUTH for the historical\n * `LogApiCall.isUserError` behavior (which now delegates here), so the classification lives in one\n * place.\n *\n * The question is \"are things WORKING?\", NOT \"was it an HTTP 4xx vs 5xx\" — the two differ (see 408).\n * Classification is by portable Error TYPE, never by transport: this runs over in-process calls,\n * pubsub/queue handlers, and HTTP through one code path and only ever sees a thrown Error. The Http*\n * classes are portable error types that travel with the throw anywhere, so matching the TYPE works\n * with or without any HTTP in the picture.\n *\n * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:\n * - HttpBadRequestError (400), HttpUnauthorizedError (401), HttpForbiddenError (403),\n * HttpNotFoundError (404) → the server is fine, the caller erred → NON-failure.\n * SERVER — something may actually be WRONG, so SURFACE it (failure):\n * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately\n * absent below, so it counts as a failure. 500/502/504/598 and any non-Http Error: real failures.\n *\n * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected \"user made a mistake\".\n * CLIENT: receiving ANY error except 266 means the outbound call FAILED → failure.\n */\nexport class WebpiecesDefaultFailureClassifier implements FailureClassifier {\n /**\n * Terminal tier — NEVER returns undefined (a definitive verdict is required so classification\n * always resolves to a boolean).\n *\n * @returns true = failure, false = expected/non-failure\n */\n isFailure(error: Error, methodInfo: ApiMethodInfo): boolean {\n // 266 is the one error that is never a failure, on either side.\n if (error instanceof HttpUserError) {\n return false;\n }\n // A client that RECEIVED any error (except the 266 above) made a failed call.\n if (methodInfo.side !== 'server') {\n return true;\n }\n // SERVER: only a healthy rejection of the caller's mistake is a non-failure. Note 408\n // (HttpTimeoutError) is intentionally NOT here — the client may never have seen the response.\n const healthyRejection =\n error instanceof HttpBadRequestError ||\n error instanceof HttpUnauthorizedError ||\n error instanceof HttpForbiddenError ||\n error instanceof HttpNotFoundError;\n return !healthyRejection;\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_FAILURE_CLASSIFIER = new WebpiecesDefaultFailureClassifier();\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export type { ServiceUrlDeriver } from './http/ClientRegistry';
|
|
|
28
28
|
export { ServiceInfo } from './http/ServiceInfo';
|
|
29
29
|
export { ErrorWireForm } from './http/ErrorTranslation';
|
|
30
30
|
export type { ErrorTranslation } from './http/ErrorTranslation';
|
|
31
|
+
export type { FailureClassifier } from './http/FailureClassifier';
|
|
32
|
+
export { KeyedFailureClassifier } from './http/FailureClassifier';
|
|
33
|
+
export { WebpiecesDefaultFailureClassifier, WEBPIECES_DEFAULT_FAILURE_CLASSIFIER, } from './http/WebpiecesDefaultFailureClassifier';
|
|
31
34
|
export { templateDeriver } from './http/templateDeriver';
|
|
32
35
|
export { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';
|
|
33
36
|
export { ContextReader } from './http/ContextReader';
|
package/src/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.HttpUnauthorizedError = exports.HttpBadRequestError = exports.EndpointNotFoundError = exports.HttpNotFoundError = exports.HttpError = exports.ProtocolError = exports.SECRETS = exports.Secrets = exports.METADATA_KEYS = exports.RouteMetadata = exports.AuthMeta = exports.validateNoConflictingDecorators = exports.getQueueName = exports.assertPubSubConventions = exports.assertApiKind = exports.getApiKind = exports.assertEveryEndpointHasAuthMode = exports.getAuthMode = exports.getAuthMeta = exports.isApiPath = exports.isFormPost = exports.getEndpointOptions = exports.getEndpoints = exports.getApiPath = exports.Queue = exports.PubSub = exports.Rpc = exports.AuthSharedSecret = exports.AuthOidc = exports.Auth = exports.AuthJwt = exports.Public = exports.AuthenticationConfig = exports.Authentication = exports.Endpoint = exports.ApiPath = exports.GCP_LOG_BUDGET_BYTES = exports.MAX_GCP_LOG_BYTES = exports.LogChunkInfo = exports.LogChunkerImpl = exports.LogChunker = exports.LogManager = exports.ConsoleLoggerFactory = exports.ConsoleLogger = exports.DESIGN_METADATA_KEYS = exports.isDocumentDesign = exports.DocumentDesign = exports.ContextTuple = exports.ContextKey = exports.toError = void 0;
|
|
12
|
-
exports.SerializedError = exports.SerializedMap = exports.RecordSerializer = exports.getDoNotRecordFields = exports.DoNotRecord = exports.RecordedTestCase = exports.RecordedError = exports.RecordedEndpoint = exports.RecorderKeys = exports.ApiCallContextHolder = exports.ApiMethodInfo = exports.LOG_API_CALL_LOGGER_NAME = exports.ApiCallLogNameImpl = exports.ApiCallLogName = exports.ApiCallInfo = exports.LogApiCallImpl = exports.LogApiCall = exports.ContextMgr = exports.WebpiecesCoreHeaders = exports.templateDeriver = exports.ErrorWireForm = exports.ServiceInfo = exports.ClientRegistry = exports.HeaderRegistry = exports.DateTimeUtil = exports.TimeUtil = exports.DateUtil = exports.InstantUtil = 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.HttpUserError = exports.HttpVendorError = exports.HttpInternalServerError = exports.HttpGatewayTimeoutError = exports.HttpBadGatewayError = exports.HttpTimeoutError = exports.HttpForbiddenError = void 0;
|
|
12
|
+
exports.SerializedError = exports.SerializedMap = exports.RecordSerializer = exports.getDoNotRecordFields = exports.DoNotRecord = exports.RecordedTestCase = exports.RecordedError = exports.RecordedEndpoint = exports.RecorderKeys = exports.ApiCallContextHolder = exports.ApiMethodInfo = exports.LOG_API_CALL_LOGGER_NAME = exports.ApiCallLogNameImpl = exports.ApiCallLogName = exports.ApiCallInfo = exports.LogApiCallImpl = exports.LogApiCall = exports.ContextMgr = exports.WebpiecesCoreHeaders = exports.templateDeriver = exports.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER = exports.WebpiecesDefaultFailureClassifier = exports.KeyedFailureClassifier = exports.ErrorWireForm = exports.ServiceInfo = exports.ClientRegistry = exports.HeaderRegistry = exports.DateTimeUtil = exports.TimeUtil = exports.DateUtil = exports.InstantUtil = 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.HttpUserError = exports.HttpVendorError = exports.HttpInternalServerError = exports.HttpGatewayTimeoutError = exports.HttpBadGatewayError = exports.HttpTimeoutError = exports.HttpForbiddenError = void 0;
|
|
13
13
|
var errorUtils_1 = require("./lib/errorUtils");
|
|
14
14
|
Object.defineProperty(exports, "toError", { enumerable: true, get: function () { return errorUtils_1.toError; } });
|
|
15
15
|
var ContextKey_1 = require("./ContextKey");
|
|
@@ -119,6 +119,11 @@ Object.defineProperty(exports, "ServiceInfo", { enumerable: true, get: function
|
|
|
119
119
|
// ClientRegistry at startup; consulted before the built-in webpieces mapping on BOTH sides.
|
|
120
120
|
var ErrorTranslation_1 = require("./http/ErrorTranslation");
|
|
121
121
|
Object.defineProperty(exports, "ErrorWireForm", { enumerable: true, get: function () { return ErrorTranslation_1.ErrorWireForm; } });
|
|
122
|
+
var FailureClassifier_1 = require("./http/FailureClassifier");
|
|
123
|
+
Object.defineProperty(exports, "KeyedFailureClassifier", { enumerable: true, get: function () { return FailureClassifier_1.KeyedFailureClassifier; } });
|
|
124
|
+
var WebpiecesDefaultFailureClassifier_1 = require("./http/WebpiecesDefaultFailureClassifier");
|
|
125
|
+
Object.defineProperty(exports, "WebpiecesDefaultFailureClassifier", { enumerable: true, get: function () { return WebpiecesDefaultFailureClassifier_1.WebpiecesDefaultFailureClassifier; } });
|
|
126
|
+
Object.defineProperty(exports, "WEBPIECES_DEFAULT_FAILURE_CLASSIFIER", { enumerable: true, get: function () { return WebpiecesDefaultFailureClassifier_1.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER; } });
|
|
122
127
|
var templateDeriver_1 = require("./http/templateDeriver");
|
|
123
128
|
Object.defineProperty(exports, "templateDeriver", { enumerable: true, get: function () { return templateDeriver_1.templateDeriver; } });
|
|
124
129
|
var WebpiecesCoreHeaders_1 = require("./http/WebpiecesCoreHeaders");
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @webpieces/core-util\n *\n * Utility functions for WebPieces applications.\n * This package works in both browser and Node.js environments.\n *\n * @packageDocumentation\n */\n\nexport { toError } from './lib/errorUtils';\nexport { ContextKey } from './ContextKey';\nexport { ContextTuple } from './ContextTuple';\n\n// @DocumentDesign — DI-design-root marker. Applies to ANY project kind (server\n// controllers AND library impl classes), so it lives here (browser + Node) rather\n// than in a server-only routing package.\nexport { DocumentDesign, isDocumentDesign, DESIGN_METADATA_KEYS } from './DocumentDesign';\n\n// Logging (merged from former @webpieces/wp-logging).\n// Pluggable logging interface + a browser-safe console default; apps plug in\n// bunyan/winston/pino/etc. via LogManager.setFactory(...). Browser + Node.\nexport type { Logger, LogLevel } from './logging/Logger';\nexport type { LoggerFactory } from './logging/LoggerFactory';\nexport { ConsoleLogger } from './logging/ConsoleLogger';\nexport { ConsoleLoggerFactory } from './logging/ConsoleLoggerFactory';\nexport { LogManager } from './logging/LogManager';\nexport { LogChunker, LogChunkerImpl, LogChunkInfo, MAX_GCP_LOG_BYTES, GCP_LOG_BUDGET_BYTES } from './logging/LogChunker';\n\n// HTTP API contract (merged from former @webpieces/http-api).\n// Shared HTTP API definition consumed by both client and server: REST\n// decorators, the HttpError hierarchy, datetime DTOs, platform-header\n// registry/readers, ValidateImplementation, and the test-case recorder\n// contract. Pure definitions — express-free, browser + Node safe.\n\n// API definition decorators\nexport {\n ApiPath,\n Endpoint,\n Authentication,\n AuthenticationConfig,\n // Auth mode decorators (clean service-to-service + user JWT model)\n Public,\n AuthJwt,\n Auth,\n AuthOidc,\n AuthSharedSecret,\n // API kind (RPC vs PubSub/Cloud Tasks) + queue naming\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n isFormPost,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n validateNoConflictingDecorators,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n} from './http/decorators';\nexport type { AuthMode, ApiKind, JwtRequirement, EndpointOptions } from './http/decorators';\n// Client-side shared-secret store (the value THIS service sends per @AuthSharedSecret key).\nexport { Secrets, SECRETS } from './http/Secrets';\n\n// Type validators\nexport { ValidateImplementation } from './http/validators';\n\n// HTTP errors\nexport {\n ProtocolError,\n HttpError,\n HttpNotFoundError,\n EndpointNotFoundError,\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpTimeoutError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n HttpInternalServerError,\n HttpVendorError,\n HttpUserError,\n // Error subtype constants\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 './http/errors';\n\n// Date/Time DTOs and Utilities (inspired by Java Time / JSR-310)\nexport {\n InstantDto,\n DateDto,\n TimeDto,\n DateTimeDto,\n InstantUtil,\n DateUtil,\n TimeUtil,\n DateTimeUtil,\n} from './http/datetime';\n\n// Context keys + registry (the global magic-context header system)\nexport { HeaderRegistry } from './http/HeaderRegistry';\nexport { ClientRegistry } from './http/ClientRegistry';\nexport type { ServiceUrlDeriver } from './http/ClientRegistry';\n\n// \"What service am I\" — set once at startup, read by the logging backends and by\n// RequestContextHeaders (to stamp requestIdSource on ids this service mints).\nexport { ServiceInfo } from './http/ServiceInfo';\n// Pluggable, bidirectional error translation (app exception <-> wire form). Registered on\n// ClientRegistry at startup; consulted before the built-in webpieces mapping on BOTH sides.\nexport { ErrorWireForm } from './http/ErrorTranslation';\nexport type { ErrorTranslation } from './http/ErrorTranslation';\n// Pluggable per-client failure classification (is a thrown API-call error a real failure or an\n// expected non-failure?). Registered on ClientRegistry at startup; consulted by LogApiCall.\nexport type { FailureClassifier } from './http/FailureClassifier';\nexport { KeyedFailureClassifier } from './http/FailureClassifier';\nexport {\n WebpiecesDefaultFailureClassifier,\n WEBPIECES_DEFAULT_FAILURE_CLASSIFIER,\n} from './http/WebpiecesDefaultFailureClassifier';\nexport { templateDeriver } from './http/templateDeriver';\nexport { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';\nexport { ContextReader } from './http/ContextReader';\n\n// BROWSER-ONLY outbound-header propagation (app-held store + registry -> outbound HTTP headers).\n// Only @webpieces/http-client-browser may name it; the server reads RequestContext directly via\n// RequestContextHeaders in the Node-only @webpieces/core-context.\nexport { ContextMgr } from './http/ContextMgr';\n\n// API-call logging helper (uses LogManager above). Singleton: use the LogApiCall constant, not `new`.\nexport { LogApiCall, LogApiCallImpl } from './http/LogApiCall';\n\n// The structured `api` tag + the context-writer seam LogApiCall stamps through. The Node\n// RequestContext-backed impl is installed by @webpieces/core-context; the browser gets the no-op.\nexport { ApiCallInfo } from './http/ApiCallInfo';\nexport type { ApiType, ApiResult } from './http/ApiCallInfo';\n// Console-render bridge: turns LogApiCall's [LogApiCall] bracket into [API.{side}.{phase}] locally.\nexport { ApiCallLogName, ApiCallLogNameImpl, LOG_API_CALL_LOGGER_NAME } from './http/ApiCallLogName';\nexport { ApiMethodInfo } from './http/ApiMethodInfo';\nexport type { ApiSide } from './http/ApiMethodInfo';\nexport { ApiCallContextHolder } from './http/ApiCallContext';\nexport type { ApiCallContext } from './http/ApiCallContext';\n\n// Test-case recording contract (impl lives in http-server; hooks in http-client)\nexport { TestCaseRecorder, RecorderKeys } from './http/recorder/TestCaseRecorder';\nexport { RecordedEndpoint, RecordedError, RecordedTestCase } from './http/recorder/RecordedEndpoint';\nexport { DoNotRecord, getDoNotRecordFields } from './http/recorder/DoNotRecord';\nexport { RecordSerializer, SerializedMap, SerializedError } from './http/recorder/RecordSerializer';\n"]}
|