@owlmeans/api 0.1.18-rc.35 → 0.1.18-rc.37

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/README.md CHANGED
@@ -12,7 +12,7 @@ HTTP API client service for OwlMeans client contexts — carries entrypoint call
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bun add @owlmeans/api@^0.1.18-rc.35
15
+ bun add @owlmeans/api@^0.1.18-rc.37
16
16
  ```
17
17
 
18
18
  ## Usage
@@ -65,7 +65,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
65
65
  your project's skill store (`.agents/skills/`):
66
66
 
67
67
  ```sh
68
- npx @owlmeans/agent-skills@^0.1.18-rc.35
68
+ npx @owlmeans/agent-skills@^0.1.18-rc.38
69
69
  ```
70
70
 
71
71
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/api",
4
- "version": "0.1.18-rc.35",
5
- "generatedAt": "2026-09-22T21:57:18.841Z",
4
+ "version": "0.1.18-rc.37",
5
+ "generatedAt": "2026-09-23T19:57:40.715Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: api
3
- description: How to use @owlmeans/api — the axios-based HTTP client service that carries entrypoint calls between services, its status-to-outcome mapping, typed transport errors and per-request timeout/abort. Auto-invoked when importing the API client, or when ctx.entrypoint(...).call() under the hood is involved.
3
+ description: How to use @owlmeans/api — the axios-based HTTP client service that carries entrypoint calls between services, its status-to-outcome mapping, typed transport errors that keep the HTTP status and incident id (the ./status subpath), and per-request timeout/abort. Auto-invoked when importing the API client, reading a failed call's status, or when ctx.entrypoint(...).call() under the hood is involved.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/api
9
9
 
10
10
  **Layer:** Core
11
- **Install:** `"@owlmeans/api": "^0.1.18-rc.35"` in `dependencies`
11
+ **Install:** `"@owlmeans/api": "^0.1.18-rc.37"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
@@ -17,8 +17,13 @@ user-invocable: false
17
17
  | `createApiService(alias?)` | Factory for the HTTP client service (axios) |
18
18
  | `appendApiClient(ctx, alias?)` | Register it and make it the context's default `webService` when none is set |
19
19
  | `ApiClient` | Service interface — a single `handler(req, reply)` |
20
- | `ApiError`, `ApiClientError`, `ServerCrashedError`, `ServerAuthError` | Typed transport errors |
21
- | Constants | Status codes (`OK`, `CREATED`, `ACCEPTED`, `FINISHED`, `UNAUTHORIZED_ERROR`, `FORBIDDEN_ERROR`, `SERVER_ERROR`), `DEFAULT_ALIAS` (`web-client`) |
20
+ | `ApiError`, `ApiClientError`, `ServerCrashedError`, `ServerAuthError`, `ApiStatusError` | Typed transport errors; an `ApiClientError` carries the answered `status` and the server's `incidentId` |
21
+ | `httpStatusOf`, `incidentIdOf`, `isIncidentBody`, `parseClientMarker`, `API_CLIENT_MARKER`, `API_STATUS_MARKER` | Read a failure's HTTP status and incident id (also the `./status` subpath) |
22
+ | Constants | Status codes (`OK`, `CREATED`, `ACCEPTED`, `FINISHED`, `UNAUTHORIZED_ERROR`, `FORBIDDEN_ERROR`, `SERVER_ERROR`), `INCIDENT_ID_HEADER` (`X-Incident-ID`), `DEFAULT_ALIAS` (`web-client`) |
23
+
24
+ Subpath `./status` — `httpStatusOf`, `incidentIdOf`, `isIncidentBody`, `parseClientMarker`, the
25
+ markers and `INCIDENT_ID_HEADER`, importing only `@owlmeans/error`: a browser package reads a
26
+ status without pulling axios in.
22
27
 
23
28
  ## How a call is carried
24
29
 
@@ -56,10 +61,25 @@ included, comes back and is mapped here.
56
61
  | 201 / 204 | Resolves with the body, or with the response headers when the body is empty |
57
62
  | any other | Rejects |
58
63
 
59
- A rejection prefers the server's own error: a text body is rehydrated into the original
60
- `ResilientError`, so a `handleError` reply on the far side arrives as the same class it was thrown
61
- as. Failing that it becomes `ServerCrashedError` (500), `ServerAuthError` (401) or `ApiClientError`
62
- (403 and the rest).
64
+ Every rejection keeps the HTTP status and, when the server sent one, its incident id (the
65
+ `X-Incident-ID` header, read from `AxiosHeaders` or a plain object, else a bare UUID body — a
66
+ browser reads that header cross-origin only when the server exposes it, the body covers the rest):
67
+
68
+ | Body | Rejects with |
69
+ |---|---|
70
+ | Development — the marshaled `ResilientError` (carries the separator) | The original class, rebuilt through the registry, stamped `responseStatus` and `incidentId` |
71
+ | Production — a bare incident UUID; also proxy HTML, framework JSON, anything else | `ServerCrashedError` `api:client:crashed:<id>` (500) · `ServerAuthError` `api:client:auth:<id>` (401) · `ApiClientError` `api:client:forbidden[:<id>]` (403) · `ApiStatusError` `api:client:status:<n>[:<id>]` (every other status) |
72
+
73
+ An `@owlmeans/server-api` boundary in production exposure sends ONLY the incident id, so a typed
74
+ refusal's class never reaches the browser — its status does. Read it with `httpStatusOf(e)`: the
75
+ stamped `responseStatus`, else an `ApiClientError`'s parsed `status`, else a class's declared
76
+ `httpStatus` (a 4xx; a 5xx only with `allowServerErrorStatus`, exactly what the server answers),
77
+ else an `api:client:*` marker in the message or type; `null` when nothing states one. A consumer
78
+ that acts on a refusal checks the class/marker AND the status (`consentRefusalOf(e) ||
79
+ httpStatusOf(e) === 428`). Markers without an id stay as they were (`api:client:crashed:error`,
80
+ `api:client:forbidden`). The status and id are rebuilt from the marker in `finalizeUnmarshal()`,
81
+ so they survive a marshal. No class of this family declares a static `httpStatus`: a server that
82
+ rethrows one still answers 500.
63
83
 
64
84
  **A failure with no answer at all is a different family.** Suppressing status errors does not wrap
65
85
  the call: an expired `timeout` (`ECONNABORTED`), an aborted `signal` (`CanceledError` /
package/build/consts.d.ts CHANGED
@@ -5,6 +5,11 @@ export declare const OK = 200;
5
5
  export declare const CREATED = 201;
6
6
  export declare const ACCEPTED = 202;
7
7
  export declare const FINISHED = 204;
8
+ /**
9
+ * The response header an `@owlmeans/server-api` boundary puts its incident id in — the key to the
10
+ * server log entry that owns a failure's full detail.
11
+ */
12
+ export declare const INCIDENT_ID_HEADER = "X-Incident-ID";
8
13
  export declare const DEFAULT_ALIAS = "web-client";
9
14
  export declare const protocols: string[];
10
15
  //# sourceMappingURL=consts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,YAAY,MAAM,CAAA;AAE/B,eAAO,MAAM,kBAAkB,MAAM,CAAA;AAErC,eAAO,MAAM,eAAe,MAAM,CAAA;AAElC,eAAO,MAAM,EAAE,MAAM,CAAA;AAErB,eAAO,MAAM,OAAO,MAAM,CAAA;AAE1B,eAAO,MAAM,QAAQ,MAAM,CAAA;AAE3B,eAAO,MAAM,QAAQ,MAAM,CAAA;AAE3B,eAAO,MAAM,aAAa,eAAe,CAAA;AAEzC,eAAO,MAAM,SAAS,UAAiC,CAAA"}
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,YAAY,MAAM,CAAA;AAE/B,eAAO,MAAM,kBAAkB,MAAM,CAAA;AAErC,eAAO,MAAM,eAAe,MAAM,CAAA;AAElC,eAAO,MAAM,EAAE,MAAM,CAAA;AAErB,eAAO,MAAM,OAAO,MAAM,CAAA;AAE1B,eAAO,MAAM,QAAQ,MAAM,CAAA;AAE3B,eAAO,MAAM,QAAQ,MAAM,CAAA;AAE3B;;;GAGG;AACH,eAAO,MAAM,kBAAkB,kBAAkB,CAAA;AAEjD,eAAO,MAAM,aAAa,eAAe,CAAA;AAEzC,eAAO,MAAM,SAAS,UAAiC,CAAA"}
package/build/consts.js CHANGED
@@ -5,6 +5,11 @@ export const OK = 200;
5
5
  export const CREATED = 201;
6
6
  export const ACCEPTED = 202;
7
7
  export const FINISHED = 204;
8
+ /**
9
+ * The response header an `@owlmeans/server-api` boundary puts its incident id in — the key to the
10
+ * server log entry that owns a failure's full detail.
11
+ */
12
+ export const INCIDENT_ID_HEADER = 'X-Incident-ID';
8
13
  export const DEFAULT_ALIAS = 'web-client';
9
14
  export const protocols = ['http', 'https', 'wss', 'ws'];
10
15
  //# sourceMappingURL=consts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;AAE/B,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAA;AAErC,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAA;AAElC,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,CAAA;AAErB,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAA;AAE1B,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAA;AAE3B,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAA;AAE3B,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAA;AAEzC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA"}
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;AAE/B,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAA;AAErC,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAA;AAElC,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,CAAA;AAErB,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAA;AAE1B,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAA;AAE3B,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAA;AAE3B;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAA;AAEjD,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAA;AAEzC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA"}
package/build/errors.d.ts CHANGED
@@ -3,16 +3,44 @@ export declare class ApiError extends ResilientError {
3
3
  static typeName: string;
4
4
  constructor(message?: string);
5
5
  }
6
+ /**
7
+ * A request the peer answered with a failure.
8
+ *
9
+ * `status` is the HTTP status the marker in the message states (`api:client:crashed:<id>` is 500,
10
+ * `api:client:auth:<id>` 401, `api:client:forbidden[:<id>]` 403, `api:client:status:<n>[:<id>]`
11
+ * any other); `incidentId` is the server's correlation id when the marker carries one. Only
12
+ * `type` and `message` survive a marshal, so both are rebuilt from the message in
13
+ * `finalizeUnmarshal()`. No class of this family declares a static `httpStatus`: a server that
14
+ * rethrows one answers 500, as before.
15
+ */
6
16
  export declare class ApiClientError extends ApiError {
7
17
  static typeName: string;
18
+ /** The HTTP status the peer answered with, when the message states one. */
19
+ status?: number;
8
20
  constructor(message?: string);
21
+ protected applyStatus(): void;
22
+ finalizeUnmarshal(): void;
9
23
  }
24
+ /** The peer answered 500. Marker `api:client:crashed:<incident id | error>`. */
10
25
  export declare class ServerCrashedError extends ApiClientError {
11
26
  static typeName: string;
12
- constructor(message?: string);
27
+ constructor(incidentId?: string);
13
28
  }
29
+ /** The peer answered 401. Marker `api:client:auth:<incident id | error>`. */
14
30
  export declare class ServerAuthError extends ApiClientError {
15
31
  static typeName: string;
16
- constructor(message?: string);
32
+ constructor(incidentId?: string);
33
+ }
34
+ /**
35
+ * The peer answered any other non-2xx status with no typed error in the body — a production
36
+ * incident id, a proxy's HTML page, a framework's JSON. Marker `api:client:status:<n>[:<id>]`.
37
+ */
38
+ export declare class ApiStatusError extends ApiClientError {
39
+ static typeName: string;
40
+ /** The marker body: `<status>[:<incident id>]`. */
41
+ static encode(status: number, incidentId?: string): string;
42
+ constructor(status: number, incidentId?: string);
43
+ /** A marker body (`ApiStatusError.encode`) or a marshaled message — the error registry's path. */
44
+ constructor(message: string);
17
45
  }
18
46
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,qBAAa,QAAS,SAAQ,cAAc;IAC1C,OAAuB,QAAQ,SAAa;IAE5C,YAAY,OAAO,GAAE,MAAgB,EAEpC;CACF;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAAmB;IAElD,YAAY,OAAO,GAAE,MAAgB,EAGpC;CACF;AAED,qBAAa,kBAAmB,SAAQ,cAAc;IACpD,OAAuB,QAAQ,SAA4C;IAE3E,YAAY,OAAO,GAAE,MAAgB,EAGpC;CACF;AAED,qBAAa,eAAgB,SAAQ,cAAc;IACjD,OAAuB,QAAQ,SAAyC;IAExE,YAAY,OAAO,GAAE,MAAgB,EAGpC;CACF"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAGhD,qBAAa,QAAS,SAAQ,cAAc;IAC1C,OAAuB,QAAQ,SAAa;IAE5C,YAAY,OAAO,GAAE,MAAgB,EAEpC;CACF;AAED;;;;;;;;;GASG;AACH,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAAmB;IAElD,2EAA2E;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IAEtB,YAAY,OAAO,GAAE,MAAgB,EAIpC;IAED,SAAS,CAAC,WAAW,IAAI,IAAI,CAM5B;IAEQ,iBAAiB,IAAI,IAAI,CAEjC;CACF;AAED,gFAAgF;AAChF,qBAAa,kBAAmB,SAAQ,cAAc;IACpD,OAAuB,QAAQ,SAA4C;IAE3E,YAAY,UAAU,CAAC,EAAE,MAAM,EAG9B;CACF;AAED,6EAA6E;AAC7E,qBAAa,eAAgB,SAAQ,cAAc;IACjD,OAAuB,QAAQ,SAAyC;IAExE,YAAY,UAAU,CAAC,EAAE,MAAM,EAG9B;CACF;AAED;;;GAGG;AACH,qBAAa,cAAe,SAAQ,cAAc;IAChD,OAAuB,QAAQ,SAAqC;IAEpE,mDAAmD;IACnD,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzD;IAED,YAAY,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAC;IAChD,kGAAkG;IAClG,YAAY,OAAO,EAAE,MAAM,EAAC;CAK7B"}
package/build/errors.js CHANGED
@@ -1,33 +1,75 @@
1
1
  import { ResilientError } from '@owlmeans/error';
2
+ import { parseClientMarker } from './status/marker.js';
2
3
  export class ApiError extends ResilientError {
3
4
  static typeName = 'ApiError';
4
5
  constructor(message = 'error') {
5
6
  super(ApiError.typeName, `api:${message}`);
6
7
  }
7
8
  }
9
+ /**
10
+ * A request the peer answered with a failure.
11
+ *
12
+ * `status` is the HTTP status the marker in the message states (`api:client:crashed:<id>` is 500,
13
+ * `api:client:auth:<id>` 401, `api:client:forbidden[:<id>]` 403, `api:client:status:<n>[:<id>]`
14
+ * any other); `incidentId` is the server's correlation id when the marker carries one. Only
15
+ * `type` and `message` survive a marshal, so both are rebuilt from the message in
16
+ * `finalizeUnmarshal()`. No class of this family declares a static `httpStatus`: a server that
17
+ * rethrows one answers 500, as before.
18
+ */
8
19
  export class ApiClientError extends ApiError {
9
20
  static typeName = 'ApiClientError';
21
+ /** The HTTP status the peer answered with, when the message states one. */
22
+ status;
10
23
  constructor(message = 'error') {
11
24
  super(`client:${message}`);
12
25
  this.type = ApiClientError.typeName;
26
+ this.applyStatus();
27
+ }
28
+ applyStatus() {
29
+ const marker = parseClientMarker(this.message);
30
+ this.status = marker?.status;
31
+ if (marker?.incidentId != null) {
32
+ this.incidentId ??= marker.incidentId;
33
+ }
34
+ }
35
+ finalizeUnmarshal() {
36
+ this.applyStatus();
13
37
  }
14
38
  }
39
+ /** The peer answered 500. Marker `api:client:crashed:<incident id | error>`. */
15
40
  export class ServerCrashedError extends ApiClientError {
16
41
  static typeName = `${ApiClientError.typeName}ServerCrashed`;
17
- constructor(message = 'error') {
18
- super(`crashed:${message}`);
42
+ constructor(incidentId) {
43
+ super(`crashed:${incidentId ?? 'error'}`);
19
44
  this.type = ServerCrashedError.typeName;
20
45
  }
21
46
  }
47
+ /** The peer answered 401. Marker `api:client:auth:<incident id | error>`. */
22
48
  export class ServerAuthError extends ApiClientError {
23
49
  static typeName = `${ApiClientError.typeName}ServerAuth`;
24
- constructor(message = 'error') {
25
- super(`auth:${message}`);
50
+ constructor(incidentId) {
51
+ super(`auth:${incidentId ?? 'error'}`);
26
52
  this.type = ServerAuthError.typeName;
27
53
  }
28
54
  }
55
+ /**
56
+ * The peer answered any other non-2xx status with no typed error in the body — a production
57
+ * incident id, a proxy's HTML page, a framework's JSON. Marker `api:client:status:<n>[:<id>]`.
58
+ */
59
+ export class ApiStatusError extends ApiClientError {
60
+ static typeName = `${ApiClientError.typeName}Status`;
61
+ /** The marker body: `<status>[:<incident id>]`. */
62
+ static encode(status, incidentId) {
63
+ return incidentId != null && incidentId !== '' ? `${status}:${incidentId}` : `${status}`;
64
+ }
65
+ constructor(status = 'error', incidentId) {
66
+ super(`status:${typeof status === 'number' ? ApiStatusError.encode(status, incidentId) : status}`);
67
+ this.type = ApiStatusError.typeName;
68
+ }
69
+ }
29
70
  ResilientError.registerErrorClass(ApiError);
30
71
  ResilientError.registerErrorClass(ApiClientError);
31
72
  ResilientError.registerErrorClass(ServerCrashedError);
32
73
  ResilientError.registerErrorClass(ServerAuthError);
74
+ ResilientError.registerErrorClass(ApiStatusError);
33
75
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,OAAO,QAAS,SAAQ,cAAc;IACnC,MAAM,CAAU,QAAQ,GAAG,UAAU,CAAA;IAE5C,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,OAAO,EAAE,CAAC,CAAA;IAC5C,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,gBAAgB,CAAA;IAElD,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;CACF;AAED,MAAM,OAAO,kBAAmB,SAAQ,cAAc;IAC7C,MAAM,CAAU,QAAQ,GAAG,GAAG,cAAc,CAAC,QAAQ,eAAe,CAAA;IAE3E,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,WAAW,OAAO,EAAE,CAAC,CAAA;QAC3B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC,QAAQ,CAAA;IACzC,CAAC;CACF;AAED,MAAM,OAAO,eAAgB,SAAQ,cAAc;IAC1C,MAAM,CAAU,QAAQ,GAAG,GAAG,cAAc,CAAC,QAAQ,YAAY,CAAA;IAExE,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAA;QACxB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAA;IACtC,CAAC;CACF;AAED,cAAc,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;AAC3C,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA;AACjD,cAAc,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAA;AACrD,cAAc,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAA"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEtD,MAAM,OAAO,QAAS,SAAQ,cAAc;IACnC,MAAM,CAAU,QAAQ,GAAG,UAAU,CAAA;IAE5C,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,OAAO,EAAE,CAAC,CAAA;IAC5C,CAAC;CACF;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,gBAAgB,CAAA;IAElD,2EAA2E;IACpE,MAAM,CAAS;IAEtB,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;QACnC,IAAI,CAAC,WAAW,EAAE,CAAA;IACpB,CAAC;IAES,WAAW;QACnB,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC9C,IAAI,CAAC,MAAM,GAAG,MAAM,EAAE,MAAM,CAAA;QAC5B,IAAI,MAAM,EAAE,UAAU,IAAI,IAAI,EAAE,CAAC;YAC/B,IAAI,CAAC,UAAU,KAAK,MAAM,CAAC,UAAU,CAAA;QACvC,CAAC;IACH,CAAC;IAEQ,iBAAiB;QACxB,IAAI,CAAC,WAAW,EAAE,CAAA;IACpB,CAAC;CACF;AAED,gFAAgF;AAChF,MAAM,OAAO,kBAAmB,SAAQ,cAAc;IAC7C,MAAM,CAAU,QAAQ,GAAG,GAAG,cAAc,CAAC,QAAQ,eAAe,CAAA;IAE3E,YAAY,UAAmB;QAC7B,KAAK,CAAC,WAAW,UAAU,IAAI,OAAO,EAAE,CAAC,CAAA;QACzC,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC,QAAQ,CAAA;IACzC,CAAC;CACF;AAED,6EAA6E;AAC7E,MAAM,OAAO,eAAgB,SAAQ,cAAc;IAC1C,MAAM,CAAU,QAAQ,GAAG,GAAG,cAAc,CAAC,QAAQ,YAAY,CAAA;IAExE,YAAY,UAAmB;QAC7B,KAAK,CAAC,QAAQ,UAAU,IAAI,OAAO,EAAE,CAAC,CAAA;QACtC,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAA;IACtC,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,cAAe,SAAQ,cAAc;IACzC,MAAM,CAAU,QAAQ,GAAG,GAAG,cAAc,CAAC,QAAQ,QAAQ,CAAA;IAEpE,mDAAmD;IACnD,MAAM,CAAC,MAAM,CAAC,MAAc,EAAE,UAAmB;QAC/C,OAAO,UAAU,IAAI,IAAI,IAAI,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,CAAA;IAC1F,CAAC;IAKD,YAAY,MAAM,GAAoB,OAAO,EAAE,UAAmB;QAChE,KAAK,CAAC,UAAU,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;QAClG,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;CACF;AAED,cAAc,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;AAC3C,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA;AACjD,cAAc,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAA;AACrD,cAAc,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAA;AAClD,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA"}
package/build/index.d.ts CHANGED
@@ -2,4 +2,6 @@ export * from './errors.js';
2
2
  export * from './consts.js';
3
3
  export * from './service.js';
4
4
  export type * from './types.js';
5
+ export { httpStatusOf, incidentIdOf, isIncidentBody, parseClientMarker, API_CLIENT_MARKER, API_STATUS_MARKER, } from './status/index.js';
6
+ export type { ClientMarker, ResponseStatusCarrier } from './status/index.js';
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,mBAAmB,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,mBAAmB,YAAY,CAAA;AAC/B,OAAO,EACL,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,GACpG,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAA"}
package/build/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './errors.js';
2
2
  export * from './consts.js';
3
3
  export * from './service.js';
4
+ export { httpStatusOf, incidentIdOf, isIncidentBody, parseClientMarker, API_CLIENT_MARKER, API_STATUS_MARKER, } from './status/index.js';
4
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAE5B,OAAO,EACL,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,GACpG,MAAM,mBAAmB,CAAA"}
@@ -0,0 +1,27 @@
1
+ export { INCIDENT_ID_HEADER } from '../consts.js';
2
+ export { API_CLIENT_MARKER, API_STATUS_MARKER, parseClientMarker } from './marker.js';
3
+ export type { ClientMarker } from './marker.js';
4
+ /** The HTTP status the API client stamps on an error it rebuilt from a response body. */
5
+ export interface ResponseStatusCarrier {
6
+ responseStatus?: number;
7
+ }
8
+ /**
9
+ * The HTTP status an error answers to, or `null` when nothing states one.
10
+ *
11
+ * Read in order: the status the API client stamped on the error it rebuilt from a response
12
+ * (`responseStatus`); the status an `ApiClientError` parsed from its marker; the `httpStatus` its
13
+ * class declares — a 4xx always, a 5xx only with `allowServerErrorStatus`, exactly what an
14
+ * `@owlmeans/server-api` boundary answers with; finally an `api:client:*` marker in the message or
15
+ * the type of anything else. This is what lets a browser recognise a refusal from its status
16
+ * alone when a production body carries nothing but an incident id.
17
+ */
18
+ export declare const httpStatusOf: (error: unknown) => number | null;
19
+ /** The server's incident id carried by an error — its own field, else its marker — or `null`. */
20
+ export declare const incidentIdOf: (error: unknown) => string | null;
21
+ /**
22
+ * Whether a response body is a production incident body — the bare incident UUID an
23
+ * `@owlmeans/server-api` boundary sends instead of the marshaled error. A development body
24
+ * carries the `ResilientError` separator and is never one.
25
+ */
26
+ export declare const isIncidentBody: (data: unknown) => data is string;
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/status/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACrF,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,yFAAyF;AACzF,MAAM,WAAW,qBAAqB;IACpC,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAaD;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,UAAW,OAAO,KAAG,MAAM,GAAG,IAkBtD,CAAA;AAED,iGAAiG;AACjG,eAAO,MAAM,YAAY,UAAW,OAAO,KAAG,MAAM,GAAG,IAUtD,CAAA;AAID;;;;GAIG;AACH,eAAO,MAAM,cAAc,SAAU,OAAO,KAAG,IAAI,IAAI,MACyC,CAAA"}
@@ -0,0 +1,59 @@
1
+ import { ResilientError } from '@owlmeans/error';
2
+ import { ApiClientError } from '../errors.js';
3
+ import { parseClientMarker } from './marker.js';
4
+ export { INCIDENT_ID_HEADER } from '../consts.js';
5
+ export { API_CLIENT_MARKER, API_STATUS_MARKER, parseClientMarker } from './marker.js';
6
+ const httpStatus = (value) => typeof value === 'number' && Number.isInteger(value) && value >= 100 && value <= 599 ? value : null;
7
+ /** The first `api:client:*` marker in an error's `message`, else in its `type`. */
8
+ const markerOf = (error) => {
9
+ const { message, type } = error;
10
+ return (typeof message === 'string' ? parseClientMarker(message) : null)
11
+ ?? (typeof type === 'string' ? parseClientMarker(type) : null);
12
+ };
13
+ /**
14
+ * The HTTP status an error answers to, or `null` when nothing states one.
15
+ *
16
+ * Read in order: the status the API client stamped on the error it rebuilt from a response
17
+ * (`responseStatus`); the status an `ApiClientError` parsed from its marker; the `httpStatus` its
18
+ * class declares — a 4xx always, a 5xx only with `allowServerErrorStatus`, exactly what an
19
+ * `@owlmeans/server-api` boundary answers with; finally an `api:client:*` marker in the message or
20
+ * the type of anything else. This is what lets a browser recognise a refusal from its status
21
+ * alone when a production body carries nothing but an incident id.
22
+ */
23
+ export const httpStatusOf = (error) => {
24
+ if (error == null || typeof error !== 'object') {
25
+ return null;
26
+ }
27
+ const stamped = httpStatus(error.responseStatus);
28
+ if (stamped != null) {
29
+ return stamped;
30
+ }
31
+ if (error instanceof ApiClientError && httpStatus(error.status) != null) {
32
+ return error.status;
33
+ }
34
+ const declaration = error.constructor;
35
+ const declared = httpStatus(declaration?.httpStatus);
36
+ if (declared != null && (declared < 500 || declaration?.allowServerErrorStatus === true)) {
37
+ return declared;
38
+ }
39
+ return markerOf(error)?.status ?? null;
40
+ };
41
+ /** The server's incident id carried by an error — its own field, else its marker — or `null`. */
42
+ export const incidentIdOf = (error) => {
43
+ if (error == null || typeof error !== 'object') {
44
+ return null;
45
+ }
46
+ const own = error.incidentId;
47
+ if (typeof own === 'string' && own !== '') {
48
+ return own;
49
+ }
50
+ return markerOf(error)?.incidentId ?? null;
51
+ };
52
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
53
+ /**
54
+ * Whether a response body is a production incident body — the bare incident UUID an
55
+ * `@owlmeans/server-api` boundary sends instead of the marshaled error. A development body
56
+ * carries the `ResilientError` separator and is never one.
57
+ */
58
+ export const isIncidentBody = (data) => typeof data === 'string' && !data.includes(ResilientError.separator) && UUID.test(data.trim());
59
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/status/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /** The prefix every `ApiClientError` message carries. */
2
+ export declare const API_CLIENT_MARKER = "api:client:";
3
+ /** The marker of an `ApiStatusError`: `api:client:status:<n>[:<incident id>]`. */
4
+ export declare const API_STATUS_MARKER = "api:client:status:";
5
+ /** What an `api:client:*` marker states. */
6
+ export interface ClientMarker {
7
+ status?: number;
8
+ incidentId?: string;
9
+ }
10
+ /** Read the first `api:client:*` marker in a text, or `null` when it carries none. */
11
+ export declare const parseClientMarker: (text: string) => ClientMarker | null;
12
+ //# sourceMappingURL=marker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marker.d.ts","sourceRoot":"","sources":["../../src/status/marker.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,eAAO,MAAM,iBAAiB,gBAAgB,CAAA;AAE9C,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,uBAAgC,CAAA;AAY9D,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,sFAAsF;AACtF,eAAO,MAAM,iBAAiB,SAAU,MAAM,KAAG,YAAY,GAAG,IAa/D,CAAA"}
@@ -0,0 +1,27 @@
1
+ /** The prefix every `ApiClientError` message carries. */
2
+ export const API_CLIENT_MARKER = 'api:client:';
3
+ /** The marker of an `ApiStatusError`: `api:client:status:<n>[:<incident id>]`. */
4
+ export const API_STATUS_MARKER = `${API_CLIENT_MARKER}status:`;
5
+ /**
6
+ * `crashed` / `auth` / `forbidden` / `status:<n>` / the legacy bare `<n>`, then an optional
7
+ * `:<incident id>`. The look-ahead refuses a match that runs on into more marker text, so a
8
+ * doubled prefix (a subclass constructor handed a whole marshaled message) resolves to the inner,
9
+ * complete marker.
10
+ */
11
+ const MARKER = /api:client:(?:(crashed|auth|forbidden)|status:(\d{3})|(\d{3}))(?::([\w.-]{1,128}))?(?![\w.:-])/;
12
+ const NAMED_STATUS = { crashed: 500, auth: 401, forbidden: 403 };
13
+ /** Read the first `api:client:*` marker in a text, or `null` when it carries none. */
14
+ export const parseClientMarker = (text) => {
15
+ const match = MARKER.exec(text);
16
+ if (match == null) {
17
+ return null;
18
+ }
19
+ const [, named, coded, legacy, id] = match;
20
+ const code = named != null ? NAMED_STATUS[named] : Number(coded ?? legacy);
21
+ return {
22
+ status: code >= 100 && code <= 599 ? code : undefined,
23
+ // `error` is the placeholder a marker gets when no incident id was known.
24
+ incidentId: id != null && id !== 'error' ? id : undefined,
25
+ };
26
+ };
27
+ //# sourceMappingURL=marker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marker.js","sourceRoot":"","sources":["../../src/status/marker.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAA;AAE9C,kFAAkF;AAClF,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,iBAAiB,SAAS,CAAA;AAE9D;;;;;GAKG;AACH,MAAM,MAAM,GAAG,gGAAgG,CAAA;AAE/G,MAAM,YAAY,GAA2B,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAA;AAQxF,sFAAsF;AACtF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,IAAY,EAAuB,EAAE;IACrE,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC/B,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,GAAG,KAAK,CAAA;IAC1C,MAAM,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,CAAA;IAE1E,OAAO;QACL,MAAM,EAAE,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;QACrD,0EAA0E;QAC1E,UAAU,EAAE,EAAE,IAAI,IAAI,IAAI,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS;KAC1D,CAAA;AACH,CAAC,CAAA"}
@@ -1,4 +1,11 @@
1
1
  import type { AbstractResponse } from '@owlmeans/entrypoint';
2
2
  import type { AxiosResponse } from 'axios';
3
+ import { ApiClientError } from '../errors.js';
3
4
  export declare const processResponse: (response: AxiosResponse, reply: AbstractResponse<any>) => void;
5
+ /**
6
+ * The typed error of a bare status: `ServerCrashedError` (500), `ServerAuthError` (401),
7
+ * `ApiClientError('forbidden[:<id>]')` (403), `ApiStatusError` (`api:client:status:<n>[:<id>]`)
8
+ * for every other one.
9
+ */
10
+ export declare const statusError: (status: number, incidentId?: string) => ApiClientError;
4
11
  //# sourceMappingURL=handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/utils/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAE5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAK1C,eAAO,MAAM,eAAe,aAAc,aAAa,SAAS,gBAAgB,CAAC,GAAG,CAAC,SAyCpF,CAAA"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/utils/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAE5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAK1C,OAAO,EAAE,cAAc,EAAuD,MAAM,cAAc,CAAA;AAIlG,eAAO,MAAM,eAAe,aAAc,aAAa,SAAS,gBAAgB,CAAC,GAAG,CAAC,SAoBpF,CAAA;AA6BD;;;;GAIG;AACH,eAAO,MAAM,WAAW,WAAY,MAAM,eAAe,MAAM,KAAG,cAUjE,CAAA"}
@@ -1,7 +1,8 @@
1
1
  import { EntrypointOutcome } from '@owlmeans/entrypoint';
2
- import { ACCEPTED, CREATED, FINISHED, FORBIDDEN_ERROR, OK, SERVER_ERROR, UNAUTHORIZED_ERROR } from '../consts.js';
2
+ import { ACCEPTED, CREATED, FINISHED, FORBIDDEN_ERROR, INCIDENT_ID_HEADER, OK, SERVER_ERROR, UNAUTHORIZED_ERROR, } from '../consts.js';
3
3
  import { ResilientError } from '@owlmeans/error';
4
- import { ApiClientError, ServerAuthError, ServerCrashedError } from '../errors.js';
4
+ import { ApiClientError, ApiStatusError, ServerAuthError, ServerCrashedError } from '../errors.js';
5
+ import { isIncidentBody } from '../status/index.js';
5
6
  export const processResponse = (response, reply) => {
6
7
  switch (response.status) {
7
8
  case OK:
@@ -16,33 +17,61 @@ export const processResponse = (response, reply) => {
16
17
  return processEmptyResponse(response, reply, EntrypointOutcome.Finished);
17
18
  default:
18
19
  console.error(response.status, response.statusText);
19
- console.error(response.config.url, response.headers);
20
+ console.error(response.config?.url, response.headers);
20
21
  console.error(response.data);
21
- try {
22
- if (typeof response.data === 'string') {
23
- reply.reject(ResilientError.ensure(response.data, true));
24
- return;
25
- }
26
- }
27
- catch {
28
- console.error(`Server returned an unrecognizable text error: ${response.status} ${response.data}`);
29
- }
30
- switch (response.status) {
31
- case SERVER_ERROR:
32
- reply.reject(new ServerCrashedError());
33
- return;
34
- case UNAUTHORIZED_ERROR:
35
- reply.reject(new ServerAuthError());
36
- return;
37
- case FORBIDDEN_ERROR:
38
- reply.reject(new ApiClientError('forbidden'));
39
- return;
40
- default:
41
- reply.reject(new ApiClientError(`${response.status}`));
42
- return;
43
- }
22
+ reply.reject(failureOf(response));
44
23
  }
45
24
  };
25
+ /**
26
+ * The error a non-2xx answer rejects with — always carrying the HTTP status and, when the server
27
+ * sent one, its incident id.
28
+ *
29
+ * A development body is the marshaled `ResilientError`: it is rebuilt into its own class and
30
+ * stamped with `responseStatus`. Anything else — the bare incident id of a production body, a
31
+ * proxy's HTML, a framework's JSON — becomes the status error of {@link statusError}.
32
+ */
33
+ const failureOf = (response) => {
34
+ const incidentId = headerOf(response.headers, INCIDENT_ID_HEADER)
35
+ ?? (isIncidentBody(response.data) ? response.data.trim() : undefined);
36
+ if (typeof response.data === 'string' && response.data.includes(ResilientError.separator)) {
37
+ try {
38
+ const error = ResilientError.ensure(response.data, true);
39
+ error.incidentId ??= incidentId;
40
+ Object.assign(error, { responseStatus: response.status });
41
+ return error;
42
+ }
43
+ catch {
44
+ console.error(`Server returned an unrecognizable text error: ${response.status} ${response.data}`);
45
+ }
46
+ }
47
+ return statusError(response.status, incidentId);
48
+ };
49
+ /**
50
+ * The typed error of a bare status: `ServerCrashedError` (500), `ServerAuthError` (401),
51
+ * `ApiClientError('forbidden[:<id>]')` (403), `ApiStatusError` (`api:client:status:<n>[:<id>]`)
52
+ * for every other one.
53
+ */
54
+ export const statusError = (status, incidentId) => {
55
+ const error = status === SERVER_ERROR ? new ServerCrashedError(incidentId)
56
+ : status === UNAUTHORIZED_ERROR ? new ServerAuthError(incidentId)
57
+ : status === FORBIDDEN_ERROR ? new ApiClientError(incidentId != null ? `forbidden:${incidentId}` : 'forbidden')
58
+ : new ApiStatusError(status, incidentId);
59
+ if (incidentId != null) {
60
+ error.incidentId = incidentId;
61
+ }
62
+ return Object.assign(error, { responseStatus: status });
63
+ };
64
+ /** One response header, from an `AxiosHeaders` instance or a plain header object alike. */
65
+ const headerOf = (headers, name) => {
66
+ if (headers == null || typeof headers !== 'object') {
67
+ return undefined;
68
+ }
69
+ const get = headers.get;
70
+ const viaGet = typeof get === 'function' ? get.call(headers, name) : undefined;
71
+ const value = viaGet ?? Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
72
+ const first = Array.isArray(value) ? value[0] : value;
73
+ return typeof first === 'string' && first.trim() !== '' ? first.trim() : undefined;
74
+ };
46
75
  const processEmptyResponse = (response, reply, outcome) => {
47
76
  if (response.data != null && response.data != '') {
48
77
  reply.resolve(response.data, outcome);
@@ -1 +1 @@
1
- {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/utils/handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/utils/handler.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@owlmeans/api",
3
- "version": "0.1.18-rc.35",
3
+ "version": "0.1.18-rc.37",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsc -b",
8
8
  "dev": "sleep 0 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
- "watch": "tsc -b -w --preserveWatchOutput --pretty"
9
+ "watch": "tsc -b -w --preserveWatchOutput --pretty",
10
+ "test": "bun test ./tests"
10
11
  },
11
12
  "main": "build/index.js",
12
13
  "module": "build/index.js",
@@ -18,17 +19,24 @@
18
19
  "default": "./build/index.js",
19
20
  "module": "./build/index.js",
20
21
  "types": "./build/index.d.ts"
22
+ },
23
+ "./status": {
24
+ "import": "./build/status/index.js",
25
+ "require": "./build/status/index.js",
26
+ "default": "./build/status/index.js",
27
+ "module": "./build/status/index.js",
28
+ "types": "./build/status/index.d.ts"
21
29
  }
22
30
  },
23
31
  "dependencies": {
24
- "@owlmeans/auth-common": "^0.1.18-rc.35",
25
- "@owlmeans/client-config": "^0.1.18-rc.35",
26
- "@owlmeans/client-route": "^0.1.18-rc.36",
27
- "@owlmeans/config": "^0.1.18-rc.35",
28
- "@owlmeans/context": "^0.1.18-rc.31",
29
- "@owlmeans/error": "^0.1.18-rc.33",
30
- "@owlmeans/entrypoint": "^0.1.18-rc.34",
31
- "@owlmeans/route": "^0.1.18-rc.32",
32
+ "@owlmeans/auth-common": "^0.1.18-rc.37",
33
+ "@owlmeans/client-config": "^0.1.18-rc.37",
34
+ "@owlmeans/client-route": "^0.1.18-rc.38",
35
+ "@owlmeans/config": "^0.1.18-rc.37",
36
+ "@owlmeans/context": "^0.1.18-rc.32",
37
+ "@owlmeans/error": "^0.1.18-rc.35",
38
+ "@owlmeans/entrypoint": "^0.1.18-rc.36",
39
+ "@owlmeans/route": "^0.1.18-rc.33",
32
40
  "axios": "^1.7.7",
33
41
  "qs": "^6.13.0"
34
42
  },
package/src/consts.ts CHANGED
@@ -13,6 +13,12 @@ export const ACCEPTED = 202
13
13
 
14
14
  export const FINISHED = 204
15
15
 
16
+ /**
17
+ * The response header an `@owlmeans/server-api` boundary puts its incident id in — the key to the
18
+ * server log entry that owns a failure's full detail.
19
+ */
20
+ export const INCIDENT_ID_HEADER = 'X-Incident-ID'
21
+
16
22
  export const DEFAULT_ALIAS = 'web-client'
17
23
 
18
24
  export const protocols = ['http', 'https', 'wss', 'ws']
package/src/errors.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ResilientError } from '@owlmeans/error'
2
+ import { parseClientMarker } from './status/marker.js'
2
3
 
3
4
  export class ApiError extends ResilientError {
4
5
  public static override typeName = 'ApiError'
@@ -8,34 +9,84 @@ export class ApiError extends ResilientError {
8
9
  }
9
10
  }
10
11
 
12
+ /**
13
+ * A request the peer answered with a failure.
14
+ *
15
+ * `status` is the HTTP status the marker in the message states (`api:client:crashed:<id>` is 500,
16
+ * `api:client:auth:<id>` 401, `api:client:forbidden[:<id>]` 403, `api:client:status:<n>[:<id>]`
17
+ * any other); `incidentId` is the server's correlation id when the marker carries one. Only
18
+ * `type` and `message` survive a marshal, so both are rebuilt from the message in
19
+ * `finalizeUnmarshal()`. No class of this family declares a static `httpStatus`: a server that
20
+ * rethrows one answers 500, as before.
21
+ */
11
22
  export class ApiClientError extends ApiError {
12
23
  public static override typeName = 'ApiClientError'
13
24
 
25
+ /** The HTTP status the peer answered with, when the message states one. */
26
+ public status?: number
27
+
14
28
  constructor(message: string = 'error') {
15
29
  super(`client:${message}`)
16
30
  this.type = ApiClientError.typeName
31
+ this.applyStatus()
32
+ }
33
+
34
+ protected applyStatus(): void {
35
+ const marker = parseClientMarker(this.message)
36
+ this.status = marker?.status
37
+ if (marker?.incidentId != null) {
38
+ this.incidentId ??= marker.incidentId
39
+ }
40
+ }
41
+
42
+ override finalizeUnmarshal(): void {
43
+ this.applyStatus()
17
44
  }
18
45
  }
19
46
 
47
+ /** The peer answered 500. Marker `api:client:crashed:<incident id | error>`. */
20
48
  export class ServerCrashedError extends ApiClientError {
21
49
  public static override typeName = `${ApiClientError.typeName}ServerCrashed`
22
50
 
23
- constructor(message: string = 'error') {
24
- super(`crashed:${message}`)
51
+ constructor(incidentId?: string) {
52
+ super(`crashed:${incidentId ?? 'error'}`)
25
53
  this.type = ServerCrashedError.typeName
26
54
  }
27
55
  }
28
56
 
57
+ /** The peer answered 401. Marker `api:client:auth:<incident id | error>`. */
29
58
  export class ServerAuthError extends ApiClientError {
30
59
  public static override typeName = `${ApiClientError.typeName}ServerAuth`
31
60
 
32
- constructor(message: string = 'error') {
33
- super(`auth:${message}`)
61
+ constructor(incidentId?: string) {
62
+ super(`auth:${incidentId ?? 'error'}`)
34
63
  this.type = ServerAuthError.typeName
35
64
  }
36
65
  }
37
66
 
67
+ /**
68
+ * The peer answered any other non-2xx status with no typed error in the body — a production
69
+ * incident id, a proxy's HTML page, a framework's JSON. Marker `api:client:status:<n>[:<id>]`.
70
+ */
71
+ export class ApiStatusError extends ApiClientError {
72
+ public static override typeName = `${ApiClientError.typeName}Status`
73
+
74
+ /** The marker body: `<status>[:<incident id>]`. */
75
+ static encode(status: number, incidentId?: string): string {
76
+ return incidentId != null && incidentId !== '' ? `${status}:${incidentId}` : `${status}`
77
+ }
78
+
79
+ constructor(status: number, incidentId?: string)
80
+ /** A marker body (`ApiStatusError.encode`) or a marshaled message — the error registry's path. */
81
+ constructor(message: string)
82
+ constructor(status: number | string = 'error', incidentId?: string) {
83
+ super(`status:${typeof status === 'number' ? ApiStatusError.encode(status, incidentId) : status}`)
84
+ this.type = ApiStatusError.typeName
85
+ }
86
+ }
87
+
38
88
  ResilientError.registerErrorClass(ApiError)
39
89
  ResilientError.registerErrorClass(ApiClientError)
40
90
  ResilientError.registerErrorClass(ServerCrashedError)
41
91
  ResilientError.registerErrorClass(ServerAuthError)
92
+ ResilientError.registerErrorClass(ApiStatusError)
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
-
2
1
  export * from './errors.js'
3
2
  export * from './consts.js'
4
3
  export * from './service.js'
5
4
  export type * from './types.js'
5
+ export {
6
+ httpStatusOf, incidentIdOf, isIncidentBody, parseClientMarker, API_CLIENT_MARKER, API_STATUS_MARKER,
7
+ } from './status/index.js'
8
+ export type { ClientMarker, ResponseStatusCarrier } from './status/index.js'
@@ -0,0 +1,76 @@
1
+ import { ResilientError } from '@owlmeans/error'
2
+ import { ApiClientError } from '../errors.js'
3
+ import { parseClientMarker } from './marker.js'
4
+
5
+ export { INCIDENT_ID_HEADER } from '../consts.js'
6
+ export { API_CLIENT_MARKER, API_STATUS_MARKER, parseClientMarker } from './marker.js'
7
+ export type { ClientMarker } from './marker.js'
8
+
9
+ /** The HTTP status the API client stamps on an error it rebuilt from a response body. */
10
+ export interface ResponseStatusCarrier {
11
+ responseStatus?: number
12
+ }
13
+
14
+ const httpStatus = (value: unknown): number | null =>
15
+ typeof value === 'number' && Number.isInteger(value) && value >= 100 && value <= 599 ? value : null
16
+
17
+ /** The first `api:client:*` marker in an error's `message`, else in its `type`. */
18
+ const markerOf = (error: object) => {
19
+ const { message, type } = error as { message?: unknown, type?: unknown }
20
+
21
+ return (typeof message === 'string' ? parseClientMarker(message) : null)
22
+ ?? (typeof type === 'string' ? parseClientMarker(type) : null)
23
+ }
24
+
25
+ /**
26
+ * The HTTP status an error answers to, or `null` when nothing states one.
27
+ *
28
+ * Read in order: the status the API client stamped on the error it rebuilt from a response
29
+ * (`responseStatus`); the status an `ApiClientError` parsed from its marker; the `httpStatus` its
30
+ * class declares — a 4xx always, a 5xx only with `allowServerErrorStatus`, exactly what an
31
+ * `@owlmeans/server-api` boundary answers with; finally an `api:client:*` marker in the message or
32
+ * the type of anything else. This is what lets a browser recognise a refusal from its status
33
+ * alone when a production body carries nothing but an incident id.
34
+ */
35
+ export const httpStatusOf = (error: unknown): number | null => {
36
+ if (error == null || typeof error !== 'object') {
37
+ return null
38
+ }
39
+ const stamped = httpStatus((error as ResponseStatusCarrier).responseStatus)
40
+ if (stamped != null) {
41
+ return stamped
42
+ }
43
+ if (error instanceof ApiClientError && httpStatus(error.status) != null) {
44
+ return error.status!
45
+ }
46
+ const declaration = error.constructor as { httpStatus?: unknown, allowServerErrorStatus?: unknown } | undefined
47
+ const declared = httpStatus(declaration?.httpStatus)
48
+ if (declared != null && (declared < 500 || declaration?.allowServerErrorStatus === true)) {
49
+ return declared
50
+ }
51
+
52
+ return markerOf(error)?.status ?? null
53
+ }
54
+
55
+ /** The server's incident id carried by an error — its own field, else its marker — or `null`. */
56
+ export const incidentIdOf = (error: unknown): string | null => {
57
+ if (error == null || typeof error !== 'object') {
58
+ return null
59
+ }
60
+ const own = (error as { incidentId?: unknown }).incidentId
61
+ if (typeof own === 'string' && own !== '') {
62
+ return own
63
+ }
64
+
65
+ return markerOf(error)?.incidentId ?? null
66
+ }
67
+
68
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
69
+
70
+ /**
71
+ * Whether a response body is a production incident body — the bare incident UUID an
72
+ * `@owlmeans/server-api` boundary sends instead of the marshaled error. A development body
73
+ * carries the `ResilientError` separator and is never one.
74
+ */
75
+ export const isIncidentBody = (data: unknown): data is string =>
76
+ typeof data === 'string' && !data.includes(ResilientError.separator) && UUID.test(data.trim())
@@ -0,0 +1,37 @@
1
+ /** The prefix every `ApiClientError` message carries. */
2
+ export const API_CLIENT_MARKER = 'api:client:'
3
+
4
+ /** The marker of an `ApiStatusError`: `api:client:status:<n>[:<incident id>]`. */
5
+ export const API_STATUS_MARKER = `${API_CLIENT_MARKER}status:`
6
+
7
+ /**
8
+ * `crashed` / `auth` / `forbidden` / `status:<n>` / the legacy bare `<n>`, then an optional
9
+ * `:<incident id>`. The look-ahead refuses a match that runs on into more marker text, so a
10
+ * doubled prefix (a subclass constructor handed a whole marshaled message) resolves to the inner,
11
+ * complete marker.
12
+ */
13
+ const MARKER = /api:client:(?:(crashed|auth|forbidden)|status:(\d{3})|(\d{3}))(?::([\w.-]{1,128}))?(?![\w.:-])/
14
+
15
+ const NAMED_STATUS: Record<string, number> = { crashed: 500, auth: 401, forbidden: 403 }
16
+
17
+ /** What an `api:client:*` marker states. */
18
+ export interface ClientMarker {
19
+ status?: number
20
+ incidentId?: string
21
+ }
22
+
23
+ /** Read the first `api:client:*` marker in a text, or `null` when it carries none. */
24
+ export const parseClientMarker = (text: string): ClientMarker | null => {
25
+ const match = MARKER.exec(text)
26
+ if (match == null) {
27
+ return null
28
+ }
29
+ const [, named, coded, legacy, id] = match
30
+ const code = named != null ? NAMED_STATUS[named] : Number(coded ?? legacy)
31
+
32
+ return {
33
+ status: code >= 100 && code <= 599 ? code : undefined,
34
+ // `error` is the placeholder a marker gets when no incident id was known.
35
+ incidentId: id != null && id !== 'error' ? id : undefined,
36
+ }
37
+ }
@@ -1,9 +1,13 @@
1
1
  import type { AbstractResponse } from '@owlmeans/entrypoint'
2
2
  import { EntrypointOutcome } from '@owlmeans/entrypoint'
3
3
  import type { AxiosResponse } from 'axios'
4
- import { ACCEPTED, CREATED, FINISHED, FORBIDDEN_ERROR, OK, SERVER_ERROR, UNAUTHORIZED_ERROR } from '../consts.js'
4
+ import {
5
+ ACCEPTED, CREATED, FINISHED, FORBIDDEN_ERROR, INCIDENT_ID_HEADER, OK, SERVER_ERROR, UNAUTHORIZED_ERROR,
6
+ } from '../consts.js'
5
7
  import { ResilientError } from '@owlmeans/error'
6
- import { ApiClientError, ServerAuthError, ServerCrashedError } from '../errors.js'
8
+ import { ApiClientError, ApiStatusError, ServerAuthError, ServerCrashedError } from '../errors.js'
9
+ import { isIncidentBody } from '../status/index.js'
10
+ import type { ResponseStatusCarrier } from '../status/index.js'
7
11
 
8
12
  export const processResponse = (response: AxiosResponse, reply: AbstractResponse<any>) => {
9
13
  switch (response.status) {
@@ -20,34 +24,70 @@ export const processResponse = (response: AxiosResponse, reply: AbstractResponse
20
24
  default:
21
25
 
22
26
  console.error(response.status, response.statusText)
23
- console.error(response.config.url, response.headers)
27
+ console.error(response.config?.url, response.headers)
24
28
  console.error(response.data)
25
29
 
26
- try {
27
- if (typeof response.data === 'string') {
28
- reply.reject(ResilientError.ensure(response.data, true))
29
- return
30
- }
31
- } catch {
32
- console.error(`Server returned an unrecognizable text error: ${response.status} ${response.data}`)
33
- }
34
- switch (response.status) {
35
- case SERVER_ERROR:
36
- reply.reject(new ServerCrashedError())
37
- return
38
- case UNAUTHORIZED_ERROR:
39
- reply.reject(new ServerAuthError())
40
- return
41
- case FORBIDDEN_ERROR:
42
- reply.reject(new ApiClientError('forbidden'))
43
- return
44
- default:
45
- reply.reject(new ApiClientError(`${response.status}`))
46
- return
47
- }
30
+ reply.reject(failureOf(response))
48
31
  }
49
32
  }
50
33
 
34
+ /**
35
+ * The error a non-2xx answer rejects with — always carrying the HTTP status and, when the server
36
+ * sent one, its incident id.
37
+ *
38
+ * A development body is the marshaled `ResilientError`: it is rebuilt into its own class and
39
+ * stamped with `responseStatus`. Anything else — the bare incident id of a production body, a
40
+ * proxy's HTML, a framework's JSON — becomes the status error of {@link statusError}.
41
+ */
42
+ const failureOf = (response: AxiosResponse): Error => {
43
+ const incidentId = headerOf(response.headers, INCIDENT_ID_HEADER)
44
+ ?? (isIncidentBody(response.data) ? response.data.trim() : undefined)
45
+
46
+ if (typeof response.data === 'string' && response.data.includes(ResilientError.separator)) {
47
+ try {
48
+ const error = ResilientError.ensure(response.data, true)
49
+ error.incidentId ??= incidentId
50
+ Object.assign(error, { responseStatus: response.status } satisfies ResponseStatusCarrier)
51
+
52
+ return error
53
+ } catch {
54
+ console.error(`Server returned an unrecognizable text error: ${response.status} ${response.data}`)
55
+ }
56
+ }
57
+
58
+ return statusError(response.status, incidentId)
59
+ }
60
+
61
+ /**
62
+ * The typed error of a bare status: `ServerCrashedError` (500), `ServerAuthError` (401),
63
+ * `ApiClientError('forbidden[:<id>]')` (403), `ApiStatusError` (`api:client:status:<n>[:<id>]`)
64
+ * for every other one.
65
+ */
66
+ export const statusError = (status: number, incidentId?: string): ApiClientError => {
67
+ const error = status === SERVER_ERROR ? new ServerCrashedError(incidentId)
68
+ : status === UNAUTHORIZED_ERROR ? new ServerAuthError(incidentId)
69
+ : status === FORBIDDEN_ERROR ? new ApiClientError(incidentId != null ? `forbidden:${incidentId}` : 'forbidden')
70
+ : new ApiStatusError(status, incidentId)
71
+ if (incidentId != null) {
72
+ error.incidentId = incidentId
73
+ }
74
+
75
+ return Object.assign(error, { responseStatus: status } satisfies ResponseStatusCarrier)
76
+ }
77
+
78
+ /** One response header, from an `AxiosHeaders` instance or a plain header object alike. */
79
+ const headerOf = (headers: unknown, name: string): string | undefined => {
80
+ if (headers == null || typeof headers !== 'object') {
81
+ return undefined
82
+ }
83
+ const get = (headers as { get?: unknown }).get
84
+ const viaGet = typeof get === 'function' ? get.call(headers, name) : undefined
85
+ const value = viaGet ?? Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1]
86
+ const first = Array.isArray(value) ? value[0] : value
87
+
88
+ return typeof first === 'string' && first.trim() !== '' ? first.trim() : undefined
89
+ }
90
+
51
91
  const processEmptyResponse = (response: AxiosResponse, reply: AbstractResponse<any>, outcome: EntrypointOutcome) => {
52
92
  if (response.data != null && response.data != '') {
53
93
  reply.resolve(response.data, outcome)
@@ -0,0 +1,186 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
2
+ import { AxiosHeaders } from 'axios'
3
+ import type { AxiosResponse } from 'axios'
4
+ import type { AbstractResponse } from '@owlmeans/entrypoint'
5
+ import { EntrypointOutcome } from '@owlmeans/entrypoint'
6
+ import { ResilientError } from '@owlmeans/error'
7
+ import {
8
+ ApiClientError, ApiStatusError, ServerAuthError, ServerCrashedError, httpStatusOf, incidentIdOf,
9
+ INCIDENT_ID_HEADER,
10
+ } from '../src/index.js'
11
+ import { processResponse, statusError } from '../src/utils/handler.js'
12
+ import * as status from '../src/status/index.js'
13
+
14
+ const INCIDENT = '6f1c1a52-8a0e-4f5e-9a7c-1f2d3e4c5b6a'
15
+
16
+ class ConsentRequired extends ResilientError {
17
+ public static override typeName = 'ApiSpecConsentRequired'
18
+ public static httpStatus = 428
19
+
20
+ constructor(message: string = 'error') {
21
+ super(ConsentRequired.typeName, `spec:consent-required:${message}`)
22
+ }
23
+ }
24
+ ResilientError.registerErrorClass(ConsentRequired)
25
+
26
+ class Unavailable extends ResilientError {
27
+ public static override typeName = 'ApiSpecUnavailable'
28
+ public static httpStatus = 503
29
+
30
+ constructor(message: string = 'error') {
31
+ super(Unavailable.typeName, message)
32
+ }
33
+ }
34
+
35
+ const respond = (
36
+ statusCode: number, data: unknown, headers: Record<string, string> | AxiosHeaders = {}
37
+ ): AbstractResponse<unknown> => {
38
+ const reply: AbstractResponse<unknown> = {
39
+ resolve: (value, outcome) => { reply.value = value; reply.outcome = outcome },
40
+ reject: error => { reply.error = error },
41
+ }
42
+ processResponse({
43
+ status: statusCode, statusText: '', data, headers, config: { url: '/spec' },
44
+ } as unknown as AxiosResponse, reply)
45
+
46
+ return reply
47
+ }
48
+
49
+ const quiet = console.error
50
+ beforeAll(() => { console.error = () => {} })
51
+ afterAll(() => { console.error = quiet })
52
+
53
+ describe('2xx', () => {
54
+ test('resolve exactly as before', () => {
55
+ expect(respond(200, { ok: true })).toMatchObject({ value: { ok: true }, outcome: EntrypointOutcome.Ok })
56
+ expect(respond(202, 'queued')).toMatchObject({ value: 'queued', outcome: EntrypointOutcome.Accepted })
57
+ expect(respond(204, '', { 'x-a': '1' })).toMatchObject({ value: { 'x-a': '1' }, outcome: EntrypointOutcome.Finished })
58
+ })
59
+ })
60
+
61
+ describe('a production incident body keeps its status and incident id', () => {
62
+ test('428 with the header becomes ApiStatusError', () => {
63
+ const { error } = respond(428, INCIDENT, { [INCIDENT_ID_HEADER.toLowerCase()]: INCIDENT })
64
+ expect(error).toBeInstanceOf(ApiStatusError)
65
+ expect(error!.message).toBe(`api:client:status:428:${INCIDENT}`)
66
+ expect(httpStatusOf(error)).toBe(428)
67
+ expect(incidentIdOf(error)).toBe(INCIDENT)
68
+ expect((error as ApiStatusError).status).toBe(428)
69
+ })
70
+
71
+ test('the header is read from AxiosHeaders too, and the body alone suffices', () => {
72
+ const headers = new AxiosHeaders({ [INCIDENT_ID_HEADER]: INCIDENT })
73
+ expect(incidentIdOf(respond(409, 'x', headers).error)).toBe(INCIDENT)
74
+ expect(incidentIdOf(respond(402, INCIDENT).error)).toBe(INCIDENT)
75
+ expect(httpStatusOf(respond(402, INCIDENT).error)).toBe(402)
76
+ })
77
+
78
+ test('500, 401 and 403 keep their classes and markers', () => {
79
+ const crashed = respond(500, INCIDENT).error
80
+ expect(crashed).toBeInstanceOf(ServerCrashedError)
81
+ expect(crashed!.message).toBe(`api:client:crashed:${INCIDENT}`)
82
+ expect(httpStatusOf(crashed)).toBe(500)
83
+
84
+ const auth = respond(401, INCIDENT).error
85
+ expect(auth).toBeInstanceOf(ServerAuthError)
86
+ expect(auth!.message).toBe(`api:client:auth:${INCIDENT}`)
87
+ expect(httpStatusOf(auth)).toBe(401)
88
+
89
+ const forbidden = respond(403, INCIDENT).error
90
+ expect(forbidden).toBeInstanceOf(ApiClientError)
91
+ expect(forbidden).not.toBeInstanceOf(ApiStatusError)
92
+ expect(forbidden!.message).toBe(`api:client:forbidden:${INCIDENT}`)
93
+ expect(httpStatusOf(forbidden)).toBe(403)
94
+ })
95
+
96
+ test('without an incident id the markers stay as before', () => {
97
+ expect(statusError(500).message).toBe('api:client:crashed:error')
98
+ expect(statusError(403).message).toBe('api:client:forbidden')
99
+ expect(incidentIdOf(statusError(500))).toBeNull()
100
+ expect(statusError(418).message).toBe('api:client:status:418')
101
+ })
102
+
103
+ test('a proxy HTML page or framework JSON becomes a status error', () => {
104
+ const html = respond(502, '<html><body>Bad Gateway</body></html>').error
105
+ expect(html).toBeInstanceOf(ApiStatusError)
106
+ expect(httpStatusOf(html)).toBe(502)
107
+ expect(incidentIdOf(html)).toBeNull()
108
+
109
+ const json = respond(400, { statusCode: 400, error: 'Bad Request', message: 'body/x' }).error
110
+ expect(json).toBeInstanceOf(ApiStatusError)
111
+ expect(httpStatusOf(json)).toBe(400)
112
+ })
113
+
114
+ test('no class declares a static httpStatus, so a rethrow upstream stays 500', () => {
115
+ for (const error of [statusError(428), statusError(500), statusError(401), statusError(403)]) {
116
+ expect((error.constructor as { httpStatus?: unknown }).httpStatus).toBeUndefined()
117
+ }
118
+ })
119
+ })
120
+
121
+ describe('a development body stays its typed class', () => {
122
+ test('rebuilt with responseStatus and incident id', () => {
123
+ const body = ResilientError.marshal(new ConsentRequired('2'), { includeStack: true, incidentId: INCIDENT }).message
124
+ const { error } = respond(428, body, { [INCIDENT_ID_HEADER]: INCIDENT })
125
+ expect(error).toBeInstanceOf(ConsentRequired)
126
+ expect(error!.message).toBe('spec:consent-required:2')
127
+ expect((error as { responseStatus?: number }).responseStatus).toBe(428)
128
+ expect(httpStatusOf(error)).toBe(428)
129
+ expect(incidentIdOf(error)).toBe(INCIDENT)
130
+ })
131
+
132
+ test('a typed api error keeps its marker fields', () => {
133
+ const body = ResilientError.marshal(new ApiStatusError(409, INCIDENT)).message
134
+ const error = respond(409, body).error as ApiStatusError
135
+ expect(error).toBeInstanceOf(ApiStatusError)
136
+ expect(error.status).toBe(409)
137
+ expect(error.incidentId).toBe(INCIDENT)
138
+ })
139
+ })
140
+
141
+ describe('marshal round trips', () => {
142
+ const roundTrip = (error: Error): ResilientError => ResilientError.ensure(ResilientError.marshal(error))
143
+
144
+ test('keep status and incident id of every client error', () => {
145
+ const cases: Array<[ApiClientError, number]> = [
146
+ [new ApiStatusError(428, INCIDENT), 428],
147
+ [new ServerCrashedError(INCIDENT), 500],
148
+ [new ServerAuthError(INCIDENT), 401],
149
+ [new ApiClientError(`forbidden:${INCIDENT}`), 403],
150
+ ]
151
+ for (const [error, code] of cases) {
152
+ const back = roundTrip(error)
153
+ expect(back.constructor).toBe(error.constructor)
154
+ expect((back as ApiClientError).status).toBe(code)
155
+ expect(httpStatusOf(back)).toBe(code)
156
+ expect(incidentIdOf(back)).toBe(INCIDENT)
157
+ }
158
+ })
159
+
160
+ test('a legacy bare-status marker still reads', () => {
161
+ expect(httpStatusOf(new ApiClientError('404'))).toBe(404)
162
+ })
163
+ })
164
+
165
+ describe('./status', () => {
166
+ test('a declared static 428 answers without any marker; an undeclared 5xx does not', () => {
167
+ expect(status.httpStatusOf(new ConsentRequired())).toBe(428)
168
+ expect(status.httpStatusOf(new Unavailable())).toBeNull()
169
+ expect(status.httpStatusOf(new Error('plain'))).toBeNull()
170
+ expect(status.httpStatusOf(null)).toBeNull()
171
+ })
172
+
173
+ test('reads a marker from a plain error message or a type', () => {
174
+ expect(status.httpStatusOf(new Error(`${status.API_STATUS_MARKER}428:${INCIDENT}`))).toBe(428)
175
+ expect(status.httpStatusOf({ type: 'api:client:auth:error' })).toBe(401)
176
+ expect(status.incidentIdOf(new Error(`api:client:crashed:${INCIDENT}`))).toBe(INCIDENT)
177
+ })
178
+
179
+ test('isIncidentBody accepts a bare incident UUID only', () => {
180
+ expect(status.isIncidentBody(INCIDENT)).toBe(true)
181
+ expect(status.isIncidentBody(`${INCIDENT}\n`)).toBe(true)
182
+ expect(status.isIncidentBody('Unauthorized')).toBe(false)
183
+ expect(status.isIncidentBody(`ApiError|||api:x|||${INCIDENT}`)).toBe(false)
184
+ expect(status.isIncidentBody({ id: INCIDENT })).toBe(false)
185
+ })
186
+ })
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": [
3
+ "@owlmeans/dep-config/tsconfig.base.json"
4
+ ],
5
+ "compilerOptions": {
6
+ "types": ["bun"],
7
+ "rootDir": "../",
8
+ "noEmit": true
9
+ },
10
+ "include": ["./**/*", "../src/**/*"]
11
+ }
package/tsconfig.json CHANGED
@@ -6,5 +6,5 @@
6
6
  "rootDir": "./src/",
7
7
  "outDir": "./build/"
8
8
  },
9
- "exclude": ["./dist/**/*", "./build/**/*", "./*.ts"]
9
+ "exclude": ["./dist/**/*", "./build/**/*", "./*.ts", "./tests/**/*"]
10
10
  }