@owlmeans/server-api 0.1.18-rc.33 → 0.1.18-rc.35
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 +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-api/SKILL.md +8 -1
- package/build/server.d.ts.map +1 -1
- package/build/server.js +2 -1
- package/build/server.js.map +1 -1
- package/build/types.d.ts +9 -0
- package/build/types.d.ts.map +1 -1
- package/build/utils/error.d.ts +22 -7
- package/build/utils/error.d.ts.map +1 -1
- package/build/utils/error.js +76 -16
- package/build/utils/error.js.map +1 -1
- package/build/utils/payload.d.ts.map +1 -1
- package/build/utils/payload.js +2 -5
- package/build/utils/payload.js.map +1 -1
- package/build/utils/server.d.ts +3 -2
- package/build/utils/server.d.ts.map +1 -1
- package/build/utils/server.js +5 -9
- package/build/utils/server.js.map +1 -1
- package/package.json +10 -10
- package/src/server.ts +2 -2
- package/src/types.ts +14 -1
- package/src/utils/error.ts +93 -16
- package/src/utils/payload.ts +2 -5
- package/src/utils/server.ts +8 -11
- package/tests/error-status.spec.ts +72 -4
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ only when composing a context without that package. WebSocket routes belong to
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
bun add @owlmeans/server-api@^0.1.18-rc.
|
|
13
|
+
bun add @owlmeans/server-api@^0.1.18-rc.35
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## Concepts
|
|
@@ -240,7 +240,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
240
240
|
your project's skill store (`.agents/skills/`):
|
|
241
241
|
|
|
242
242
|
```sh
|
|
243
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
243
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.31
|
|
244
244
|
```
|
|
245
245
|
|
|
246
246
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-api",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.35",
|
|
5
|
+
"generatedAt": "2026-09-21T21:54:22.930Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -7,7 +7,7 @@ user-invocable: false
|
|
|
7
7
|
|
|
8
8
|
# @owlmeans/server-api
|
|
9
9
|
|
|
10
|
-
**Install:** `bun add @owlmeans/server-api@^0.1.18-rc.
|
|
10
|
+
**Install:** `bun add @owlmeans/server-api@^0.1.18-rc.35`
|
|
11
11
|
|
|
12
12
|
Make handlers from the protocol declaration so input and output types stay coupled to the shared
|
|
13
13
|
contract:
|
|
@@ -102,5 +102,12 @@ body and a status from `errorStatus(error)` (`./utils`), resolved in this order:
|
|
|
102
102
|
`uploadedFile(request)` is the Fastify multipart boundary. Keep raw Fastify access there rather
|
|
103
103
|
than reaching through `request.original` in application code.
|
|
104
104
|
|
|
105
|
+
## Error exposure
|
|
106
|
+
|
|
107
|
+
`handleError` always assigns an incident UUID, attaches it to the logged error and returns it in the
|
|
108
|
+
`X-Incident-ID` response header (exposed through CORS). A production response body contains only
|
|
109
|
+
that id; an explicit `cfg.http.errors.exposure = 'development'` uses the typed marshalled form with
|
|
110
|
+
message and stack. Keep the default production-safe, and tell a client to report the incident id.
|
|
111
|
+
|
|
105
112
|
Do not use unbound compatibility handler wrappers. For a WebSocket route use
|
|
106
113
|
`@owlmeans/server-socket`'s `connection(protocol, callback)`.
|
package/build/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAkC5D,KAAK,MAAM,GAAG,YAAY,CAAA;AAK1B,eAAO,MAAM,eAAe,UAAW,MAAM,KAAG,SAkJ/C,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,OACrE,CAAC,UAAS,MAAM,KACpB,CAAC,GAAG,eAWN,CAAA"}
|
package/build/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { RouteMethod } from '@owlmeans/route';
|
|
|
5
5
|
import { createServerHandler, fixFormatDates } from './utils/index.js';
|
|
6
6
|
import { provideResponse } from '@owlmeans/entrypoint';
|
|
7
7
|
import { TOKEN_UPDATE } from '@owlmeans/auth-common';
|
|
8
|
+
import { INCIDENT_ID_HEADER } from './utils/error.js';
|
|
8
9
|
import Fastify from 'fastify';
|
|
9
10
|
import cors from '@fastify/cors';
|
|
10
11
|
import rawBody from 'fastify-raw-body';
|
|
@@ -79,7 +80,7 @@ export const createApiServer = (alias) => {
|
|
|
79
80
|
...Object.values(RouteMethod).map(method => method.toUpperCase()),
|
|
80
81
|
'HEAD', 'OPTIONS'
|
|
81
82
|
],
|
|
82
|
-
exposedHeaders: [TOKEN_UPDATE]
|
|
83
|
+
exposedHeaders: [TOKEN_UPDATE, INCIDENT_ID_HEADER]
|
|
83
84
|
});
|
|
84
85
|
await server.register(Multipart, {
|
|
85
86
|
throwFileSizeLimit: true,
|
package/build/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAClF,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE3E,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
CHANGED
|
@@ -13,7 +13,16 @@ export interface Response extends FastifyReply {
|
|
|
13
13
|
export interface ApiServerAppend {
|
|
14
14
|
getApiServer: () => ApiServer;
|
|
15
15
|
}
|
|
16
|
+
export type HttpErrorExposure = 'production' | 'development';
|
|
17
|
+
export interface HttpErrorsConfig {
|
|
18
|
+
/** Production is the safe default; development is the only mode that exposes stacks. */
|
|
19
|
+
exposure?: HttpErrorExposure;
|
|
20
|
+
}
|
|
21
|
+
export interface HttpConfig {
|
|
22
|
+
errors?: HttpErrorsConfig;
|
|
23
|
+
}
|
|
16
24
|
export interface Config extends ServerConfig {
|
|
25
|
+
http?: HttpConfig;
|
|
17
26
|
}
|
|
18
27
|
export interface Context<C extends Config = Config> extends ServerContext<C>, ApiServerAppend {
|
|
19
28
|
}
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC5E,OAAO,kCAAkC,CAAA;AAEzC,MAAM,WAAW,SAAU,SAAQ,kBAAkB;IACnD,MAAM,EAAE,eAAe,CAAA;IAEvB,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B;AAED,MAAM,WAAW,OAAQ,SAAQ,cAAc;CAAI;AAEnD,MAAM,WAAW,QAAS,SAAQ,YAAY;CAAI;AAElD,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,SAAS,CAAA;CAC9B;AAED,MAAM,WAAW,MAAO,SAAQ,YAAY;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC5E,OAAO,kCAAkC,CAAA;AAEzC,MAAM,WAAW,SAAU,SAAQ,kBAAkB;IACnD,MAAM,EAAE,eAAe,CAAA;IAEvB,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B;AAED,MAAM,WAAW,OAAQ,SAAQ,cAAc;CAAI;AAEnD,MAAM,WAAW,QAAS,SAAQ,YAAY;CAAI;AAElD,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,SAAS,CAAA;CAC9B;AAED,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,aAAa,CAAA;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,wFAAwF;IACxF,QAAQ,CAAC,EAAE,iBAAiB,CAAA;CAC7B;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,gBAAgB,CAAA;CAC1B;AAED,MAAM,WAAW,MAAO,SAAQ,YAAY;IAC1C,IAAI,CAAC,EAAE,UAAU,CAAA;CAClB;AAED,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC,EAC1E,eAAe;CAAI"}
|
package/build/utils/error.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { FastifyReply } from 'fastify';
|
|
2
|
+
import type { Config, HttpErrorExposure } from '../types.js';
|
|
3
|
+
export declare const INCIDENT_ID_HEADER = "X-Incident-ID";
|
|
2
4
|
/**
|
|
3
5
|
* What an error class may declare about the HTTP status it is answered with.
|
|
4
6
|
*
|
|
@@ -8,15 +10,17 @@ import type { FastifyReply } from 'fastify';
|
|
|
8
10
|
*/
|
|
9
11
|
export interface HttpStatusDeclaration {
|
|
10
12
|
httpStatus?: unknown;
|
|
13
|
+
/** Explicit opt-in for a declared 5xx response. Client-error declarations need no opt-in. */
|
|
14
|
+
allowServerErrorStatus?: unknown;
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
|
-
* The
|
|
17
|
+
* The HTTP status an error's class declares, or `null` when it declares none that may be honoured.
|
|
14
18
|
*
|
|
15
19
|
* Read through the constructor chain — a static property is inherited, so a subclass answers what
|
|
16
|
-
* its nearest declaring ancestor says, and redeclares to change it.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* a duplicate module copy declares exactly what the imported one does.
|
|
20
|
+
* its nearest declaring ancestor says, and redeclares to change it. An integer 4xx is honoured
|
|
21
|
+
* directly. A 5xx is honoured only with `allowServerErrorStatus = true`, so a domain class must
|
|
22
|
+
* explicitly opt in to exposing a precise availability response. The read is structural, so a
|
|
23
|
+
* class from a duplicate module copy declares exactly what the imported one does.
|
|
20
24
|
*/
|
|
21
25
|
export declare const declaredErrorStatus: (error: unknown) => number | null;
|
|
22
26
|
/**
|
|
@@ -33,10 +37,21 @@ export declare const declaredErrorStatus: (error: unknown) => number | null;
|
|
|
33
37
|
* **Order matters.** `AuthForbidden extends AuthorizationError`, so the 403 branch has to be
|
|
34
38
|
* tested first or every refusal of a permission is reported as a failure to authenticate. Both
|
|
35
39
|
* auth branches come before a class's own declaration, so an auth refusal can never be restated
|
|
36
|
-
* as something else. Everything that is neither an auth error nor a class declaring
|
|
40
|
+
* as something else. Everything that is neither an auth error nor a class declaring an allowed
|
|
37
41
|
* {@link declaredErrorStatus} answers 500.
|
|
38
42
|
*/
|
|
39
43
|
export declare const errorStatus: (error: unknown) => number;
|
|
44
|
+
export interface SerializedHttpError {
|
|
45
|
+
status: number;
|
|
46
|
+
body: string;
|
|
47
|
+
incidentId: string;
|
|
48
|
+
}
|
|
49
|
+
/** Invalid or absent configuration always resolves to the production-safe policy. */
|
|
50
|
+
export declare const errorExposure: (config?: Pick<Config, 'http'>) => HttpErrorExposure;
|
|
51
|
+
/** One central wire shape for every server error. */
|
|
52
|
+
export declare const serializeError: (error: Error, exposure?: HttpErrorExposure) => SerializedHttpError;
|
|
53
|
+
/** Apply safe response metadata carried by an error without coupling its package to Fastify. */
|
|
54
|
+
export declare const applyErrorHeaders: (error: unknown, reply: FastifyReply) => void;
|
|
40
55
|
/**
|
|
41
56
|
* Answer a thrown error: a status, and the marshalled `ResilientError` as the body.
|
|
42
57
|
*
|
|
@@ -47,5 +62,5 @@ export declare const errorStatus: (error: unknown) => number;
|
|
|
47
62
|
* Only when the thrown error answers 500 is the ENSURED error asked, because the rebuild is what
|
|
48
63
|
* gives a status to a marshalled error that crossed a hop as a plain `Error`.
|
|
49
64
|
*/
|
|
50
|
-
export declare const handleError: (error: Error, reply: FastifyReply) => void;
|
|
65
|
+
export declare const handleError: (error: Error, reply: FastifyReply, exposure?: HttpErrorExposure) => void;
|
|
51
66
|
//# sourceMappingURL=error.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/utils/error.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAK3C;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/utils/error.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAK3C,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAE5D,eAAO,MAAM,kBAAkB,kBAAkB,CAAA;AAEjD;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6FAA6F;IAC7F,sBAAsB,CAAC,EAAE,OAAO,CAAA;CACjC;AAOD;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,UAAW,OAAO,KAAG,MAAM,GAAG,IAc7D,CAAA;AAkCD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,WAAW,UAAW,OAAO,KAAG,MAkB5C,CAAA;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,qFAAqF;AACrF,eAAO,MAAM,aAAa,YAAa,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,iBACmB,CAAA;AAsBjF,qDAAqD;AACrD,eAAO,MAAM,cAAc,UAClB,KAAK,aAAY,iBAAiB,KACxC,mBAcF,CAAA;AAED,gGAAgG;AAChG,eAAO,MAAM,iBAAiB,UAAW,OAAO,SAAS,YAAY,KAAG,IAQvE,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,UACf,KAAK,SAAS,YAAY,aAAY,iBAAiB,SAY/D,CAAA"}
|
package/build/utils/error.js
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
import { FORBIDDEN_ERROR, SERVER_ERROR, UNAUTHORIZED_ERROR } from '@owlmeans/api';
|
|
2
2
|
import { AccessError, AuthFailedError } from '../errors.js';
|
|
3
3
|
import { AuthForbidden, AuthorizationError } from '@owlmeans/auth';
|
|
4
|
-
import { ResilientError } from '@owlmeans/error';
|
|
4
|
+
import { isResilientError, ResilientError, SEPARATOR } from '@owlmeans/error';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
export const INCIDENT_ID_HEADER = 'X-Incident-ID';
|
|
5
7
|
const CLIENT_ERROR_FIRST = 400;
|
|
6
8
|
const CLIENT_ERROR_LAST = 499;
|
|
9
|
+
const SERVER_ERROR_FIRST = 500;
|
|
10
|
+
const SERVER_ERROR_LAST = 599;
|
|
7
11
|
/**
|
|
8
|
-
* The
|
|
12
|
+
* The HTTP status an error's class declares, or `null` when it declares none that may be honoured.
|
|
9
13
|
*
|
|
10
14
|
* Read through the constructor chain — a static property is inherited, so a subclass answers what
|
|
11
|
-
* its nearest declaring ancestor says, and redeclares to change it.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* a duplicate module copy declares exactly what the imported one does.
|
|
15
|
+
* its nearest declaring ancestor says, and redeclares to change it. An integer 4xx is honoured
|
|
16
|
+
* directly. A 5xx is honoured only with `allowServerErrorStatus = true`, so a domain class must
|
|
17
|
+
* explicitly opt in to exposing a precise availability response. The read is structural, so a
|
|
18
|
+
* class from a duplicate module copy declares exactly what the imported one does.
|
|
15
19
|
*/
|
|
16
20
|
export const declaredErrorStatus = (error) => {
|
|
17
21
|
if (error == null || typeof error !== 'object') {
|
|
18
22
|
return null;
|
|
19
23
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
const declaration = error.constructor;
|
|
25
|
+
const status = declaration?.httpStatus;
|
|
26
|
+
if (typeof status !== 'number' || !Number.isInteger(status)) {
|
|
23
27
|
return null;
|
|
24
28
|
}
|
|
25
|
-
|
|
29
|
+
if (status >= CLIENT_ERROR_FIRST && status <= CLIENT_ERROR_LAST)
|
|
30
|
+
return status;
|
|
31
|
+
if (status >= SERVER_ERROR_FIRST && status <= SERVER_ERROR_LAST
|
|
32
|
+
&& declaration?.allowServerErrorStatus === true)
|
|
33
|
+
return status;
|
|
34
|
+
return null;
|
|
26
35
|
};
|
|
27
36
|
/**
|
|
28
37
|
* The registered type names an error carries: its own `type`, and the static `typeName` of every
|
|
@@ -66,7 +75,7 @@ const isOfFamily = (names, family) => family.some(typeName => names.has(typeName
|
|
|
66
75
|
* **Order matters.** `AuthForbidden extends AuthorizationError`, so the 403 branch has to be
|
|
67
76
|
* tested first or every refusal of a permission is reported as a failure to authenticate. Both
|
|
68
77
|
* auth branches come before a class's own declaration, so an auth refusal can never be restated
|
|
69
|
-
* as something else. Everything that is neither an auth error nor a class declaring
|
|
78
|
+
* as something else. Everything that is neither an auth error nor a class declaring an allowed
|
|
70
79
|
* {@link declaredErrorStatus} answers 500.
|
|
71
80
|
*/
|
|
72
81
|
export const errorStatus = (error) => {
|
|
@@ -87,6 +96,53 @@ export const errorStatus = (error) => {
|
|
|
87
96
|
}
|
|
88
97
|
return declaredErrorStatus(error) ?? SERVER_ERROR;
|
|
89
98
|
};
|
|
99
|
+
/** Invalid or absent configuration always resolves to the production-safe policy. */
|
|
100
|
+
export const errorExposure = (config) => config?.http?.errors?.exposure === 'development' ? 'development' : 'production';
|
|
101
|
+
/**
|
|
102
|
+
* Preserve a typed/marshalled error when possible without asking `ensure` to convert a plain
|
|
103
|
+
* error. In particular, `ensure` deliberately rethrows SyntaxError; an HTTP boundary still has
|
|
104
|
+
* to serialize it safely and let the incident log retain the original object.
|
|
105
|
+
*/
|
|
106
|
+
const normalizeForWire = (error) => {
|
|
107
|
+
if (error instanceof SyntaxError || isResilientError(error)) {
|
|
108
|
+
return error;
|
|
109
|
+
}
|
|
110
|
+
if (error.message.includes(SEPARATOR)) {
|
|
111
|
+
const ensured = ResilientError.ensure(error);
|
|
112
|
+
// The catch-all converter's legacy constructor shape shifts an ordinary error's fields.
|
|
113
|
+
// Only keep `ensure`'s result when a registered wire prefix actually rebuilt something.
|
|
114
|
+
if (ensured.type !== error.message || ensured.message !== error.stack) {
|
|
115
|
+
return ensured;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return error;
|
|
119
|
+
};
|
|
120
|
+
/** One central wire shape for every server error. */
|
|
121
|
+
export const serializeError = (error, exposure = 'production') => {
|
|
122
|
+
const normalized = normalizeForWire(error);
|
|
123
|
+
const thrown = errorStatus(error);
|
|
124
|
+
const status = thrown !== SERVER_ERROR ? thrown : errorStatus(normalized);
|
|
125
|
+
const incidentId = randomUUID();
|
|
126
|
+
// The server log owns every diagnostic. Production callers receive only its correlation key;
|
|
127
|
+
// development keeps the typed wire form so local clients can retain their usual error details.
|
|
128
|
+
Object.assign(error, { incidentId });
|
|
129
|
+
if (isResilientError(normalized))
|
|
130
|
+
normalized.incidentId = incidentId;
|
|
131
|
+
const body = exposure === 'production'
|
|
132
|
+
? incidentId
|
|
133
|
+
: ResilientError.marshal(normalized, { includeStack: true, incidentId }).message;
|
|
134
|
+
return { status, body, incidentId };
|
|
135
|
+
};
|
|
136
|
+
/** Apply safe response metadata carried by an error without coupling its package to Fastify. */
|
|
137
|
+
export const applyErrorHeaders = (error, reply) => {
|
|
138
|
+
if (error == null || typeof error !== 'object') {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const retryAfter = error.retryAfter;
|
|
142
|
+
if (typeof retryAfter === 'number' && Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
143
|
+
reply.header('Retry-After', String(Math.ceil(retryAfter)));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
90
146
|
/**
|
|
91
147
|
* Answer a thrown error: a status, and the marshalled `ResilientError` as the body.
|
|
92
148
|
*
|
|
@@ -97,12 +153,16 @@ export const errorStatus = (error) => {
|
|
|
97
153
|
* Only when the thrown error answers 500 is the ENSURED error asked, because the rebuild is what
|
|
98
154
|
* gives a status to a marshalled error that crossed a hop as a plain `Error`.
|
|
99
155
|
*/
|
|
100
|
-
export const handleError = (error, reply) => {
|
|
156
|
+
export const handleError = (error, reply, exposure = 'production') => {
|
|
101
157
|
if (!reply.sent) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
158
|
+
const serialized = serializeError(error, exposure);
|
|
159
|
+
applyErrorHeaders(error, reply);
|
|
160
|
+
// Fastify/Pino's `err` serializer retains the original type, message and full stack. This is
|
|
161
|
+
// the one log entry that owns those details in production, keyed by the response incident ID.
|
|
162
|
+
reply.log.error({ err: error, incidentId: serialized.incidentId }, 'Request failed');
|
|
163
|
+
reply.header(INCIDENT_ID_HEADER, serialized.incidentId)
|
|
164
|
+
.code(serialized.status)
|
|
165
|
+
.send(serialized.body);
|
|
106
166
|
}
|
|
107
167
|
};
|
|
108
168
|
//# sourceMappingURL=error.js.map
|
package/build/utils/error.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/utils/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAEjF,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/utils/error.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,CAAA;AAG1C,eAAO,MAAM,cAAc,UAAW,MAAM,OAAO,OAAO,cAAc,OAAO,KAAG,eAejF,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,YAAY,gBAAgB,CAAC,CAAC,CAAC,SAAS,QAAQ,iBAAiB,OAAO,KAAG,
|
|
1
|
+
{"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,CAAA;AAG1C,eAAO,MAAM,cAAc,UAAW,MAAM,OAAO,OAAO,cAAc,OAAO,KAAG,eAejF,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,YAAY,gBAAgB,CAAC,CAAC,CAAC,SAAS,QAAQ,iBAAiB,OAAO,KAAG,OAuB3G,CAAA;AAGD,eAAO,MAAM,cAAc,WAAY,eAAe,oBAgBrD,CAAA"}
|
package/build/utils/payload.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EntrypointOutcome } from '@owlmeans/entrypoint';
|
|
2
2
|
import { ACCEPTED, CREATED, OK } from '@owlmeans/api';
|
|
3
|
-
import {
|
|
3
|
+
import { handleError } from './error.js';
|
|
4
4
|
export const provideRequest = (alias, req, provision) => {
|
|
5
5
|
provision = provision ?? false;
|
|
6
6
|
return {
|
|
@@ -31,10 +31,7 @@ export const executeResponse = (response, reply, throwOnError) => {
|
|
|
31
31
|
if (throwOnError ?? false) {
|
|
32
32
|
throw response.error;
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
// caller passes `throwOnError` and lands there; a caller that does not must not report a refusal
|
|
36
|
-
// as a crash either. Nothing is ensured here, so there is no rebuilt error to ask second.
|
|
37
|
-
reply.code(errorStatus(response.error)).send(response.error.message);
|
|
34
|
+
handleError(response.error, reply);
|
|
38
35
|
return true;
|
|
39
36
|
}
|
|
40
37
|
else if (response.outcome != null) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payload.js","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAGxD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAErD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAExC,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,GAAY,EAAE,SAAmB,EAAmB,EAAE;IAClG,SAAS,GAAG,SAAS,IAAI,KAAK,CAAA;IAC9B,OAAO;QACL,KAAK;QACL,IAAI,EAAG,GAAW,CAAC,KAAK,IAAI,SAAS;QACrC,4FAA4F;QAC5F,oEAAoE;QACpE,MAAM,EAAG,GAAW,CAAC,OAAO,IAAI,SAAS;QACzC,MAAM,EAAE,GAAG,CAAC,MAA4D;QACxE,IAAI,EAAE,GAAG,CAAC,IAA2B;QACrC,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,KAAK,EAAE,GAAG,CAAC,KAA2D;QACtE,IAAI,EAAE,GAAG,CAAC,GAAG;QACb,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;KACtC,CAAA;AACH,CAAC,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAI,QAA6B,EAAE,KAAe,EAAE,YAAsB,EAAW,EAAE;IACpH,IAAI,QAAQ,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC;QAC3B,IAAI,YAAY,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,QAAQ,CAAC,KAAK,CAAA;QACtB,CAAC;QACD,
|
|
1
|
+
{"version":3,"file":"payload.js","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"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"}
|
package/build/utils/server.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
|
2
2
|
import type { ServerEntrypoint } from '@owlmeans/server-entrypoint';
|
|
3
3
|
import type { CommonEntrypoint } from '@owlmeans/entrypoint';
|
|
4
|
-
import type { ServerContext
|
|
5
|
-
type Config
|
|
4
|
+
import type { ServerContext } from '@owlmeans/server-context';
|
|
5
|
+
import type { Config as ApiConfig } from '../types.js';
|
|
6
|
+
type Config = ApiConfig;
|
|
6
7
|
type Context = ServerContext<Config>;
|
|
7
8
|
export declare const canServeModule: (context: Context, module: CommonEntrypoint) => module is ServerEntrypoint<unknown>;
|
|
8
9
|
export declare const createServerHandler: (module: ServerEntrypoint<FastifyRequest>, location: string) => (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/utils/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE3D,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AACjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAG5D,OAAO,KAAK,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/utils/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE3D,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AACjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAG5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAO7D,OAAO,KAAK,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,aAAa,CAAA;AAEtD,KAAK,MAAM,GAAG,SAAS,CAAA;AACvB,KAAK,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;AAEpC,eAAO,MAAM,cAAc,YAAa,OAAO,UAAU,gBAAgB,KAAG,MAAM,IAAI,gBAAgB,CAAC,OAAO,CAc7G,CAAA;AAED,eAAO,MAAM,mBAAmB,WAAY,gBAAgB,CAAC,cAAc,CAAC,YAAY,MAAM,WAChF,cAAc,SAAS,YAAY,kBAuC9C,CAAA"}
|
package/build/utils/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { assertContext } from '@owlmeans/context';
|
|
|
3
3
|
import { provideResponse } from '@owlmeans/entrypoint';
|
|
4
4
|
import { ResilientError } from '@owlmeans/error';
|
|
5
5
|
import { OK } from '@owlmeans/api';
|
|
6
|
-
import { handleError } from './error.js';
|
|
6
|
+
import { errorExposure, handleError } from './error.js';
|
|
7
7
|
import { executeResponse, provideRequest } from './payload.js';
|
|
8
8
|
import { authorize } from './guards.js';
|
|
9
9
|
import { RouteProtocols } from '@owlmeans/route';
|
|
@@ -14,10 +14,9 @@ export const canServeModule = (context, module) => {
|
|
|
14
14
|
if (module.route.route.service != null && module.route.route.service !== context.cfg.service) {
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|| module.route.route.protocol === RouteProtocols.QUEUE) {
|
|
17
|
+
// HTTP serves only its own protocol. Custom transports are handled by their owning packages.
|
|
18
|
+
if (module.route.route.protocol != null
|
|
19
|
+
&& module.route.route.protocol !== RouteProtocols.WEB) {
|
|
21
20
|
return false;
|
|
22
21
|
}
|
|
23
22
|
return 'isIntermediate' in module.route;
|
|
@@ -51,15 +50,12 @@ export const createServerHandler = (module, location) => async (req, reply) => {
|
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
catch (error) {
|
|
54
|
-
console.error(`Error in ${module.alias} (${location})`);
|
|
55
|
-
console.error(JSON.stringify(error, null, 2));
|
|
56
|
-
console.error(error);
|
|
57
53
|
if (module.fixer != null) {
|
|
58
54
|
const fixer = context.service(module.fixer);
|
|
59
55
|
fixer.handle(reply, ResilientError.ensure(error));
|
|
60
56
|
return;
|
|
61
57
|
}
|
|
62
|
-
handleError(error, reply);
|
|
58
|
+
handleError(error, reply, errorExposure(context.cfg));
|
|
63
59
|
}
|
|
64
60
|
};
|
|
65
61
|
//# sourceMappingURL=server.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/utils/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAE3C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAIjD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/utils/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-api",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.35",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
"@fastify/helmet": "^13.1.0",
|
|
34
34
|
"@fastify/middie": "^9.0.2",
|
|
35
35
|
"@fastify/multipart": "^9.0.1",
|
|
36
|
-
"@owlmeans/api": "^0.1.18-rc.
|
|
37
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
38
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
39
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
40
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
41
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
42
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
43
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
44
|
-
"@owlmeans/server-entrypoint": "^0.1.18-rc.
|
|
36
|
+
"@owlmeans/api": "^0.1.18-rc.31",
|
|
37
|
+
"@owlmeans/auth": "^0.1.18-rc.29",
|
|
38
|
+
"@owlmeans/auth-common": "^0.1.18-rc.31",
|
|
39
|
+
"@owlmeans/context": "^0.1.18-rc.27",
|
|
40
|
+
"@owlmeans/error": "^0.1.18-rc.29",
|
|
41
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.30",
|
|
42
|
+
"@owlmeans/route": "^0.1.18-rc.28",
|
|
43
|
+
"@owlmeans/server-context": "^0.1.18-rc.30",
|
|
44
|
+
"@owlmeans/server-entrypoint": "^0.1.18-rc.29",
|
|
45
45
|
"ajv-errors": "^3.0.0",
|
|
46
46
|
"fastify": "^5.0.0",
|
|
47
47
|
"fastify-raw-body": "^5.0.0",
|
package/src/server.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { RouteMethod } from '@owlmeans/route'
|
|
|
8
8
|
import { createServerHandler, fixFormatDates } from './utils/index.js'
|
|
9
9
|
import { provideResponse } from '@owlmeans/entrypoint'
|
|
10
10
|
import { TOKEN_UPDATE } from '@owlmeans/auth-common'
|
|
11
|
+
import { INCIDENT_ID_HEADER } from './utils/error.js'
|
|
11
12
|
|
|
12
13
|
import Fastify from 'fastify'
|
|
13
14
|
import type { FastifyRequest } from 'fastify'
|
|
@@ -99,7 +100,7 @@ export const createApiServer = (alias: string): ApiServer => {
|
|
|
99
100
|
...Object.values(RouteMethod).map(method => method.toUpperCase()),
|
|
100
101
|
'HEAD', 'OPTIONS'
|
|
101
102
|
],
|
|
102
|
-
exposedHeaders: [TOKEN_UPDATE]
|
|
103
|
+
exposedHeaders: [TOKEN_UPDATE, INCIDENT_ID_HEADER]
|
|
103
104
|
})
|
|
104
105
|
|
|
105
106
|
await server.register(Multipart, {
|
|
@@ -199,4 +200,3 @@ export const appendApiServer = <C extends Config, T extends ServerContext<C>>(
|
|
|
199
200
|
|
|
200
201
|
return context
|
|
201
202
|
}
|
|
202
|
-
|
package/src/types.ts
CHANGED
|
@@ -18,7 +18,20 @@ export interface ApiServerAppend {
|
|
|
18
18
|
getApiServer: () => ApiServer
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export
|
|
21
|
+
export type HttpErrorExposure = 'production' | 'development'
|
|
22
|
+
|
|
23
|
+
export interface HttpErrorsConfig {
|
|
24
|
+
/** Production is the safe default; development is the only mode that exposes stacks. */
|
|
25
|
+
exposure?: HttpErrorExposure
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface HttpConfig {
|
|
29
|
+
errors?: HttpErrorsConfig
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Config extends ServerConfig {
|
|
33
|
+
http?: HttpConfig
|
|
34
|
+
}
|
|
22
35
|
|
|
23
36
|
export interface Context<C extends Config = Config> extends ServerContext<C>,
|
|
24
37
|
ApiServerAppend { }
|
package/src/utils/error.ts
CHANGED
|
@@ -2,7 +2,11 @@ import { FORBIDDEN_ERROR, SERVER_ERROR, UNAUTHORIZED_ERROR } from '@owlmeans/api
|
|
|
2
2
|
import type { FastifyReply } from 'fastify'
|
|
3
3
|
import { AccessError, AuthFailedError } from '../errors.js'
|
|
4
4
|
import { AuthForbidden, AuthorizationError } from '@owlmeans/auth'
|
|
5
|
-
import { ResilientError } from '@owlmeans/error'
|
|
5
|
+
import { isResilientError, ResilientError, SEPARATOR } from '@owlmeans/error'
|
|
6
|
+
import { randomUUID } from 'node:crypto'
|
|
7
|
+
import type { Config, HttpErrorExposure } from '../types.js'
|
|
8
|
+
|
|
9
|
+
export const INCIDENT_ID_HEADER = 'X-Incident-ID'
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* What an error class may declare about the HTTP status it is answered with.
|
|
@@ -13,31 +17,38 @@ import { ResilientError } from '@owlmeans/error'
|
|
|
13
17
|
*/
|
|
14
18
|
export interface HttpStatusDeclaration {
|
|
15
19
|
httpStatus?: unknown
|
|
20
|
+
/** Explicit opt-in for a declared 5xx response. Client-error declarations need no opt-in. */
|
|
21
|
+
allowServerErrorStatus?: unknown
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
const CLIENT_ERROR_FIRST = 400
|
|
19
25
|
const CLIENT_ERROR_LAST = 499
|
|
26
|
+
const SERVER_ERROR_FIRST = 500
|
|
27
|
+
const SERVER_ERROR_LAST = 599
|
|
20
28
|
|
|
21
29
|
/**
|
|
22
|
-
* The
|
|
30
|
+
* The HTTP status an error's class declares, or `null` when it declares none that may be honoured.
|
|
23
31
|
*
|
|
24
32
|
* Read through the constructor chain — a static property is inherited, so a subclass answers what
|
|
25
|
-
* its nearest declaring ancestor says, and redeclares to change it.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* a duplicate module copy declares exactly what the imported one does.
|
|
33
|
+
* its nearest declaring ancestor says, and redeclares to change it. An integer 4xx is honoured
|
|
34
|
+
* directly. A 5xx is honoured only with `allowServerErrorStatus = true`, so a domain class must
|
|
35
|
+
* explicitly opt in to exposing a precise availability response. The read is structural, so a
|
|
36
|
+
* class from a duplicate module copy declares exactly what the imported one does.
|
|
29
37
|
*/
|
|
30
38
|
export const declaredErrorStatus = (error: unknown): number | null => {
|
|
31
39
|
if (error == null || typeof error !== 'object') {
|
|
32
40
|
return null
|
|
33
41
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
const declaration = error.constructor as HttpStatusDeclaration | undefined
|
|
43
|
+
const status = declaration?.httpStatus
|
|
44
|
+
if (typeof status !== 'number' || !Number.isInteger(status)) {
|
|
37
45
|
return null
|
|
38
46
|
}
|
|
47
|
+
if (status >= CLIENT_ERROR_FIRST && status <= CLIENT_ERROR_LAST) return status
|
|
48
|
+
if (status >= SERVER_ERROR_FIRST && status <= SERVER_ERROR_LAST
|
|
49
|
+
&& declaration?.allowServerErrorStatus === true) return status
|
|
39
50
|
|
|
40
|
-
return
|
|
51
|
+
return null
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
/**
|
|
@@ -86,7 +97,7 @@ const isOfFamily = (names: Set<string>, family: string[]): boolean =>
|
|
|
86
97
|
* **Order matters.** `AuthForbidden extends AuthorizationError`, so the 403 branch has to be
|
|
87
98
|
* tested first or every refusal of a permission is reported as a failure to authenticate. Both
|
|
88
99
|
* auth branches come before a class's own declaration, so an auth refusal can never be restated
|
|
89
|
-
* as something else. Everything that is neither an auth error nor a class declaring
|
|
100
|
+
* as something else. Everything that is neither an auth error nor a class declaring an allowed
|
|
90
101
|
* {@link declaredErrorStatus} answers 500.
|
|
91
102
|
*/
|
|
92
103
|
export const errorStatus = (error: unknown): number => {
|
|
@@ -109,6 +120,66 @@ export const errorStatus = (error: unknown): number => {
|
|
|
109
120
|
return declaredErrorStatus(error) ?? SERVER_ERROR
|
|
110
121
|
}
|
|
111
122
|
|
|
123
|
+
export interface SerializedHttpError {
|
|
124
|
+
status: number
|
|
125
|
+
body: string
|
|
126
|
+
incidentId: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Invalid or absent configuration always resolves to the production-safe policy. */
|
|
130
|
+
export const errorExposure = (config?: Pick<Config, 'http'>): HttpErrorExposure =>
|
|
131
|
+
config?.http?.errors?.exposure === 'development' ? 'development' : 'production'
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Preserve a typed/marshalled error when possible without asking `ensure` to convert a plain
|
|
135
|
+
* error. In particular, `ensure` deliberately rethrows SyntaxError; an HTTP boundary still has
|
|
136
|
+
* to serialize it safely and let the incident log retain the original object.
|
|
137
|
+
*/
|
|
138
|
+
const normalizeForWire = (error: Error): Error => {
|
|
139
|
+
if (error instanceof SyntaxError || isResilientError(error)) {
|
|
140
|
+
return error
|
|
141
|
+
}
|
|
142
|
+
if (error.message.includes(SEPARATOR)) {
|
|
143
|
+
const ensured = ResilientError.ensure(error)
|
|
144
|
+
// The catch-all converter's legacy constructor shape shifts an ordinary error's fields.
|
|
145
|
+
// Only keep `ensure`'s result when a registered wire prefix actually rebuilt something.
|
|
146
|
+
if (ensured.type !== error.message || ensured.message !== error.stack) {
|
|
147
|
+
return ensured
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return error
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** One central wire shape for every server error. */
|
|
154
|
+
export const serializeError = (
|
|
155
|
+
error: Error, exposure: HttpErrorExposure = 'production'
|
|
156
|
+
): SerializedHttpError => {
|
|
157
|
+
const normalized = normalizeForWire(error)
|
|
158
|
+
const thrown = errorStatus(error)
|
|
159
|
+
const status = thrown !== SERVER_ERROR ? thrown : errorStatus(normalized)
|
|
160
|
+
const incidentId = randomUUID()
|
|
161
|
+
// The server log owns every diagnostic. Production callers receive only its correlation key;
|
|
162
|
+
// development keeps the typed wire form so local clients can retain their usual error details.
|
|
163
|
+
Object.assign(error, { incidentId })
|
|
164
|
+
if (isResilientError(normalized)) normalized.incidentId = incidentId
|
|
165
|
+
const body = exposure === 'production'
|
|
166
|
+
? incidentId
|
|
167
|
+
: ResilientError.marshal(normalized, { includeStack: true, incidentId }).message
|
|
168
|
+
|
|
169
|
+
return { status, body, incidentId }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Apply safe response metadata carried by an error without coupling its package to Fastify. */
|
|
173
|
+
export const applyErrorHeaders = (error: unknown, reply: FastifyReply): void => {
|
|
174
|
+
if (error == null || typeof error !== 'object') {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
const retryAfter = (error as { retryAfter?: unknown }).retryAfter
|
|
178
|
+
if (typeof retryAfter === 'number' && Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
179
|
+
reply.header('Retry-After', String(Math.ceil(retryAfter)))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
112
183
|
/**
|
|
113
184
|
* Answer a thrown error: a status, and the marshalled `ResilientError` as the body.
|
|
114
185
|
*
|
|
@@ -119,11 +190,17 @@ export const errorStatus = (error: unknown): number => {
|
|
|
119
190
|
* Only when the thrown error answers 500 is the ENSURED error asked, because the rebuild is what
|
|
120
191
|
* gives a status to a marshalled error that crossed a hop as a plain `Error`.
|
|
121
192
|
*/
|
|
122
|
-
export const handleError = (
|
|
193
|
+
export const handleError = (
|
|
194
|
+
error: Error, reply: FastifyReply, exposure: HttpErrorExposure = 'production'
|
|
195
|
+
) => {
|
|
123
196
|
if (!reply.sent) {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
197
|
+
const serialized = serializeError(error, exposure)
|
|
198
|
+
applyErrorHeaders(error, reply)
|
|
199
|
+
// Fastify/Pino's `err` serializer retains the original type, message and full stack. This is
|
|
200
|
+
// the one log entry that owns those details in production, keyed by the response incident ID.
|
|
201
|
+
reply.log.error({ err: error, incidentId: serialized.incidentId }, 'Request failed')
|
|
202
|
+
reply.header(INCIDENT_ID_HEADER, serialized.incidentId)
|
|
203
|
+
.code(serialized.status)
|
|
204
|
+
.send(serialized.body)
|
|
128
205
|
}
|
|
129
206
|
}
|
package/src/utils/payload.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AbstractResponse, AbstractRequest } from '@owlmeans/entrypoint'
|
|
|
3
3
|
import type { Request, Response } from '../types.js'
|
|
4
4
|
import { ACCEPTED, CREATED, OK } from '@owlmeans/api'
|
|
5
5
|
import type { AnySchemaObject } from 'ajv'
|
|
6
|
-
import {
|
|
6
|
+
import { handleError } from './error.js'
|
|
7
7
|
|
|
8
8
|
export const provideRequest = (alias: string, req: Request, provision?: boolean): AbstractRequest => {
|
|
9
9
|
provision = provision ?? false
|
|
@@ -36,10 +36,7 @@ export const executeResponse = <T>(response: AbstractResponse<T>, reply: Respons
|
|
|
36
36
|
if (throwOnError ?? false) {
|
|
37
37
|
throw response.error
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
// caller passes `throwOnError` and lands there; a caller that does not must not report a refusal
|
|
41
|
-
// as a crash either. Nothing is ensured here, so there is no rebuilt error to ask second.
|
|
42
|
-
reply.code(errorStatus(response.error)).send(response.error.message)
|
|
39
|
+
handleError(response.error, reply)
|
|
43
40
|
return true
|
|
44
41
|
} else if (response.outcome != null) {
|
|
45
42
|
switch (response.outcome) {
|
package/src/utils/server.ts
CHANGED
|
@@ -5,15 +5,16 @@ import type { FixerService, ServerEntrypoint } from '@owlmeans/server-entrypoint
|
|
|
5
5
|
import type { CommonEntrypoint } from '@owlmeans/entrypoint'
|
|
6
6
|
import type { GateService } from '@owlmeans/entrypoint'
|
|
7
7
|
import { provideResponse } from '@owlmeans/entrypoint'
|
|
8
|
-
import type { ServerContext
|
|
8
|
+
import type { ServerContext } from '@owlmeans/server-context'
|
|
9
9
|
import { ResilientError } from '@owlmeans/error'
|
|
10
10
|
import { OK } from '@owlmeans/api'
|
|
11
|
-
import { handleError } from './error.js'
|
|
11
|
+
import { errorExposure, handleError } from './error.js'
|
|
12
12
|
import { executeResponse, provideRequest } from './payload.js'
|
|
13
13
|
import { authorize } from './guards.js'
|
|
14
14
|
import { RouteProtocols } from '@owlmeans/route'
|
|
15
|
+
import type { Config as ApiConfig } from '../types.js'
|
|
15
16
|
|
|
16
|
-
type Config =
|
|
17
|
+
type Config = ApiConfig
|
|
17
18
|
type Context = ServerContext<Config>
|
|
18
19
|
|
|
19
20
|
export const canServeModule = (context: Context, module: CommonEntrypoint): module is ServerEntrypoint<unknown> => {
|
|
@@ -23,10 +24,9 @@ export const canServeModule = (context: Context, module: CommonEntrypoint): modu
|
|
|
23
24
|
if (module.route.route.service != null && module.route.route.service !== context.cfg.service) {
|
|
24
25
|
return false
|
|
25
26
|
}
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|| module.route.route.protocol === RouteProtocols.QUEUE) {
|
|
27
|
+
// HTTP serves only its own protocol. Custom transports are handled by their owning packages.
|
|
28
|
+
if (module.route.route.protocol != null
|
|
29
|
+
&& module.route.route.protocol !== RouteProtocols.WEB) {
|
|
30
30
|
return false
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -66,14 +66,11 @@ export const createServerHandler = (module: ServerEntrypoint<FastifyRequest>, lo
|
|
|
66
66
|
reply.code(OK).send(response.value)
|
|
67
67
|
}
|
|
68
68
|
} catch (error) {
|
|
69
|
-
console.error(`Error in ${module.alias} (${location})`)
|
|
70
|
-
console.error(JSON.stringify(error, null, 2))
|
|
71
|
-
console.error(error)
|
|
72
69
|
if (module.fixer != null) {
|
|
73
70
|
const fixer: FixerService = context.service(module.fixer)
|
|
74
71
|
fixer.handle(reply, ResilientError.ensure(error as Error))
|
|
75
72
|
return
|
|
76
73
|
}
|
|
77
|
-
handleError(error as Error, reply)
|
|
74
|
+
handleError(error as Error, reply, errorExposure(context.cfg))
|
|
78
75
|
}
|
|
79
76
|
}
|
|
@@ -2,9 +2,9 @@ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import Fastify from 'fastify'
|
|
3
3
|
import type { FastifyInstance } from 'fastify'
|
|
4
4
|
import { AuthForbidden, AuthorizationError } from '@owlmeans/auth'
|
|
5
|
-
import { ResilientError } from '@owlmeans/error'
|
|
5
|
+
import { ResilientError, SEPARATOR } from '@owlmeans/error'
|
|
6
6
|
import { provideResponse } from '@owlmeans/entrypoint'
|
|
7
|
-
import { errorStatus, handleError } from '../src/utils/error.js'
|
|
7
|
+
import { errorExposure, errorStatus, handleError, INCIDENT_ID_HEADER } from '../src/utils/error.js'
|
|
8
8
|
import { executeResponse } from '../src/utils/payload.js'
|
|
9
9
|
import { AccessError, AuthFailedError } from '../src/errors.js'
|
|
10
10
|
|
|
@@ -49,6 +49,26 @@ class DeclaringForbidden extends AuthForbidden {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
class TemporarilyUnavailable extends ResilientError {
|
|
53
|
+
public static override typeName = 'ServerApiSpecTemporarilyUnavailable'
|
|
54
|
+
public static httpStatus = 503
|
|
55
|
+
public static allowServerErrorStatus = true
|
|
56
|
+
|
|
57
|
+
constructor() {
|
|
58
|
+
super(TemporarilyUnavailable.typeName, 'temporarily-unavailable')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class Throttled extends ResilientError {
|
|
63
|
+
public static override typeName = 'ServerApiSpecThrottled'
|
|
64
|
+
public static httpStatus = 429
|
|
65
|
+
public readonly retryAfter = 17.2
|
|
66
|
+
|
|
67
|
+
constructor() {
|
|
68
|
+
super(Throttled.typeName, 'throttled')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
const declaring = (typeName: string, httpStatus: unknown) => {
|
|
53
73
|
class Declaring extends ResilientError {
|
|
54
74
|
public static override typeName = typeName
|
|
@@ -136,6 +156,10 @@ beforeAll(async () => {
|
|
|
136
156
|
handleError(thrown as Error, reply)
|
|
137
157
|
return reply
|
|
138
158
|
})
|
|
159
|
+
server.get('/handle-development', async (_req, reply) => {
|
|
160
|
+
handleError(thrown as Error, reply, 'development')
|
|
161
|
+
return reply
|
|
162
|
+
})
|
|
139
163
|
server.get('/execute', async (_req, reply) => {
|
|
140
164
|
const response = provideResponse(reply)
|
|
141
165
|
response.reject(thrown as Error)
|
|
@@ -152,9 +176,41 @@ afterAll(async () => {
|
|
|
152
176
|
const answer = async (error: unknown, path: string = '/handle') => {
|
|
153
177
|
thrown = error
|
|
154
178
|
const reply = await server.inject({ method: 'GET', url: path })
|
|
155
|
-
return {
|
|
179
|
+
return {
|
|
180
|
+
status: reply.statusCode,
|
|
181
|
+
body: reply.body,
|
|
182
|
+
incidentId: reply.headers[INCIDENT_ID_HEADER.toLowerCase()],
|
|
183
|
+
retryAfter: reply.headers['retry-after'],
|
|
184
|
+
}
|
|
156
185
|
}
|
|
157
186
|
|
|
187
|
+
describe('@owlmeans/server-api — production error exposure', () => {
|
|
188
|
+
test('returns only an incident id in production and correlates it with the header', async () => {
|
|
189
|
+
const response = await answer(new Error('private-marker'))
|
|
190
|
+
expect(response.body).toBe(response.incidentId)
|
|
191
|
+
expect(response.body).not.toContain('private-marker')
|
|
192
|
+
expect(response.body).not.toContain(SEPARATOR)
|
|
193
|
+
expect(response.body).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('includes stacks only under explicit development exposure and safely serializes SyntaxError', async () => {
|
|
197
|
+
const development = await answer(new Error('development-marker'), '/handle-development')
|
|
198
|
+
const [type, message, stack, incidentId] = development.body.split(SEPARATOR, 4)
|
|
199
|
+
expect(type).toBe(ResilientError.typeName)
|
|
200
|
+
expect(message).toBe('development-marker')
|
|
201
|
+
expect(stack).toContain('development-marker')
|
|
202
|
+
expect(incidentId).toBe(development.incidentId)
|
|
203
|
+
|
|
204
|
+
const syntax = await answer(new SyntaxError('bad wiring'))
|
|
205
|
+
expect(syntax.status).toBe(500)
|
|
206
|
+
expect(syntax.body).toBe(syntax.incidentId)
|
|
207
|
+
expect(syntax.body).not.toContain('bad wiring')
|
|
208
|
+
|
|
209
|
+
expect(errorExposure()).toBe('production')
|
|
210
|
+
expect(errorExposure({ http: { errors: { exposure: 'development' } } })).toBe('development')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
158
214
|
describe('@owlmeans/server-api — handleError status', () => {
|
|
159
215
|
test('keeps the auth mappings: forbidden 403, unauthenticated 401', async () => {
|
|
160
216
|
expect((await answer(new AuthForbidden('x'))).status).toBe(403)
|
|
@@ -167,7 +223,7 @@ describe('@owlmeans/server-api — handleError status', () => {
|
|
|
167
223
|
test('honours a 4xx a class declares — inherited, redeclared, or after a marshal hop', async () => {
|
|
168
224
|
const refused = await answer(new OutOfCredit('create'))
|
|
169
225
|
expect(refused.status).toBe(402)
|
|
170
|
-
expect(
|
|
226
|
+
expect(refused.body).toBe(refused.incidentId)
|
|
171
227
|
|
|
172
228
|
expect((await answer(new OutOfCreditForStory())).status).toBe(402)
|
|
173
229
|
expect((await answer(new Busy())).status).toBe(409)
|
|
@@ -188,6 +244,18 @@ describe('@owlmeans/server-api — handleError status', () => {
|
|
|
188
244
|
}
|
|
189
245
|
})
|
|
190
246
|
|
|
247
|
+
test('honours an explicitly exposed 5xx without opening arbitrary server statuses', async () => {
|
|
248
|
+
expect((await answer(new TemporarilyUnavailable())).status).toBe(503)
|
|
249
|
+
const Unexposed = declaring('ServerApiSpecUnexposed503', 503)
|
|
250
|
+
expect((await answer(new Unexposed())).status).toBe(500)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('emits Retry-After for a throttled response', async () => {
|
|
254
|
+
const response = await answer(new Throttled())
|
|
255
|
+
expect(response.status).toBe(429)
|
|
256
|
+
expect(response.retryAfter).toBe('18')
|
|
257
|
+
})
|
|
258
|
+
|
|
191
259
|
test('executeResponse answers a rejected response with the same status', async () => {
|
|
192
260
|
expect((await answer(new OutOfCredit('create'), '/execute')).status).toBe(402)
|
|
193
261
|
expect((await answer(new AuthForbidden('x'), '/execute')).status).toBe(403)
|