@o3co/create-auth-provider 0.15.0 → 0.16.0
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.ja.md +93 -47
- package/README.md +92 -46
- package/package.json +1 -1
- package/templates/standalone/.env.example +12 -0
- package/templates/standalone/Dockerfile +1 -1
- package/templates/standalone/README.ja.md +516 -154
- package/templates/standalone/README.md +218 -91
- package/templates/standalone/config/application.conf +26 -3
- package/templates/standalone/gitignore +3 -2
- package/templates/standalone/package.json +1 -1
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +49 -15
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +1103 -0
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +241 -0
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +1100 -0
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +19 -5
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +64 -3
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +15 -0
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +133 -0
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +28 -0
- package/templates/standalone/src/__tests__/google-session.fixture.mts +304 -0
- package/templates/standalone/src/__tests__/host-routes.test.mts +141 -0
- package/templates/standalone/src/__tests__/listen.test.mts +128 -0
- package/templates/standalone/src/__tests__/logger-projection.test.mts +77 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +20 -5
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +187 -0
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +139 -0
- package/templates/standalone/src/__tests__/replica-safety.test.mts +13 -3
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +149 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +157 -20
- package/templates/standalone/src/__tests__/smoke.test.mts +39 -20
- package/templates/standalone/src/__tests__/store-credential.test.mts +276 -0
- package/templates/standalone/src/app.mts +21 -50
- package/templates/standalone/src/buildModules.mts +33 -16
- package/templates/standalone/src/listen.mts +66 -0
- package/templates/standalone/src/logger.mts +7 -4
- package/templates/standalone/src/modules.mts +42 -8
- package/templates/standalone/src/routes.mts +95 -0
- package/templates/standalone/src/shutdown.mts +20 -14
- package/templates/versions.json +15 -15
- package/templates/standalone/src/__tests__/terminal-error.test.mts +0 -96
- package/templates/standalone/src/terminalError.mts +0 -60
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* The host's routes, in the order `app.mts` mounts them: liveness, readiness
|
|
19
|
+
* and the metrics scrape ahead of the composed auth router — so they keep
|
|
20
|
+
* answering while the auth pipeline is degraded, which is when an operator
|
|
21
|
+
* needs them — then that router, then core's terminal error handler, last,
|
|
22
|
+
* because Express hands an error only to handlers mounted after the route
|
|
23
|
+
* that raised it. A function rather than inline in `app.mts`, so the order
|
|
24
|
+
* and the handler can be tested as the process mounts them.
|
|
25
|
+
*
|
|
26
|
+
* The handler is core's `terminalErrorHandler`, the one that already ends
|
|
27
|
+
* the composed router, so an error one of the host's routes lets through
|
|
28
|
+
* is answered as every other route's is: `500 server_error` /
|
|
29
|
+
* `unexpected_error` in the RFC 6749 envelope with `Cache-Control:
|
|
30
|
+
* no-store`, logged once as `unhandled_request_error` with the path through
|
|
31
|
+
* `auditErrorText` and the error's `loggableError` projection; a refusal an
|
|
32
|
+
* `http-errors` error marks as the client's keeps its 4xx; a response whose
|
|
33
|
+
* headers already went out is closed rather than handed to Express's final
|
|
34
|
+
* handler, which would print the stack.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
createHealthcheckRouter,
|
|
39
|
+
createReadinessRouter,
|
|
40
|
+
type Logger,
|
|
41
|
+
type ReadinessProbe,
|
|
42
|
+
terminalErrorHandler,
|
|
43
|
+
} from "@o3co/auth-provider-core";
|
|
44
|
+
import express, { type Express, type Router } from "express";
|
|
45
|
+
import type { Metrics } from "./metrics.mjs";
|
|
46
|
+
|
|
47
|
+
export interface MountRoutesOptions {
|
|
48
|
+
/** The router `createApp` composed (`handle.router`). */
|
|
49
|
+
readonly router: Router;
|
|
50
|
+
/** The readiness probes the builders registered (`handle.readinessProbes`). */
|
|
51
|
+
readonly probes: readonly ReadinessProbe[];
|
|
52
|
+
/** Per-probe deadline, in milliseconds: `config.http.readinessTimeoutMs`. */
|
|
53
|
+
readonly readinessTimeoutMs: number;
|
|
54
|
+
readonly metrics: Metrics;
|
|
55
|
+
readonly logger: Logger;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Mount the host's routes, the composed router and the terminal error handler on `app`. */
|
|
59
|
+
export function mountRoutes(app: Express, options: MountRoutesOptions): void {
|
|
60
|
+
// Liveness: the process is up and its event loop is turning. Deliberately
|
|
61
|
+
// static — restarting the process would not bring Redis back, so a Redis
|
|
62
|
+
// outage must not read as "this container is broken, kill it".
|
|
63
|
+
app.use(createHealthcheckRouter(express));
|
|
64
|
+
|
|
65
|
+
// Readiness: can this replica serve right now? Redis backs sessions,
|
|
66
|
+
// authorization codes and refresh-token families in the deployable
|
|
67
|
+
// defaults, so a replica that has lost it answers 503 here and should be
|
|
68
|
+
// taken out of rotation. Probes are contributed by the builders that own
|
|
69
|
+
// each connection; a memory-only deployment registers none and is always
|
|
70
|
+
// ready.
|
|
71
|
+
app.use(
|
|
72
|
+
createReadinessRouter(express, {
|
|
73
|
+
probes: options.probes,
|
|
74
|
+
timeoutMs: options.readinessTimeoutMs,
|
|
75
|
+
logger: options.logger,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Prometheus scrape endpoint. Same reasoning as the probes above: it has to
|
|
80
|
+
// keep answering while the auth pipeline is degraded, because that is when
|
|
81
|
+
// the series matter.
|
|
82
|
+
app.use(
|
|
83
|
+
options.metrics.route(express, {
|
|
84
|
+
probes: options.probes,
|
|
85
|
+
probeTimeoutMs: options.readinessTimeoutMs,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// The composed auth router, which ends in core's terminal handler.
|
|
90
|
+
app.use(options.router);
|
|
91
|
+
// Core's terminal handler again, LAST (#293 item 8), for the host routes
|
|
92
|
+
// above: Express routes an error only to handlers registered after the
|
|
93
|
+
// route that threw it. See the file header.
|
|
94
|
+
app.use(terminalErrorHandler(options.logger));
|
|
95
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Server } from "node:http";
|
|
18
|
-
import type
|
|
18
|
+
import { type Logger, loggableError } from "@o3co/auth-provider-core";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Graceful shutdown for the scaffolded server (#290).
|
|
@@ -54,13 +54,20 @@ import type { Logger } from "@o3co/auth-provider-core";
|
|
|
54
54
|
* and the process exits **non-zero** — an orchestrator that only ever sees
|
|
55
55
|
* `0` cannot tell a clean drain from one that ran out of time.
|
|
56
56
|
* 5. **`cleanup` runs after draining, before exit**, and its failure is logged
|
|
57
|
-
* through the app logger
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* through the app logger — as core's `loggableError` projection, never the
|
|
58
|
+
* error, which holds every cleanup's own error — and reflected in the exit
|
|
59
|
+
* code. It never wedges the process: a dispose that throws still exits, and
|
|
60
|
+
* one that never settles is cut off at `cleanupTimeoutMs`.
|
|
60
61
|
* 6. **A `close` that fails is not reported as a clean drain.** `server.close`
|
|
61
62
|
* reports through its callback, and treating that as success would tell an
|
|
62
63
|
* orchestrator the listener came down when it did not.
|
|
63
64
|
*
|
|
65
|
+
* Every stage is one object-first line under a snake_case event name:
|
|
66
|
+
* `shutdown_draining` (info, `drainTimeoutMs`), `shutdown_drain_deadline_exceeded`
|
|
67
|
+
* (error), `shutdown_server_close_failed` (error, `err`), `shutdown_cleanup_timed_out`
|
|
68
|
+
* (error, `cleanupTimeoutMs`), `shutdown_cleanup_failed` (error, `err`) and
|
|
69
|
+
* `shutdown_complete` (info, `reason`, `drain`, `exitCode`).
|
|
70
|
+
*
|
|
64
71
|
* Size `drainTimeoutMs` plus `cleanupTimeoutMs` **below** the orchestrator's
|
|
65
72
|
* own kill grace period (Kubernetes `terminationGracePeriodSeconds` is 30s by
|
|
66
73
|
* default, compose `stop_grace_period` 10s; with federation grants on the sum
|
|
@@ -226,7 +233,7 @@ export function installGracefulShutdown(server: Server, options: GracefulShutdow
|
|
|
226
233
|
let outcome = reason;
|
|
227
234
|
try {
|
|
228
235
|
if ((await runCleanup()) === CLEANUP_TIMED_OUT) {
|
|
229
|
-
logger.error({ cleanupTimeoutMs }, "
|
|
236
|
+
logger.error({ cleanupTimeoutMs }, "shutdown_cleanup_timed_out");
|
|
230
237
|
exitCode = 1;
|
|
231
238
|
outcome = "cleanup-timeout";
|
|
232
239
|
}
|
|
@@ -234,12 +241,14 @@ export function installGracefulShutdown(server: Server, options: GracefulShutdow
|
|
|
234
241
|
// Through the app logger, not `console.error`: a shutdown that
|
|
235
242
|
// failed to release its Redis connections is exactly the line an
|
|
236
243
|
// operator needs to find later, and a bare write is the one their
|
|
237
|
-
// pipeline drops.
|
|
238
|
-
|
|
244
|
+
// pipeline drops. The projection, not the error: `dispose()`
|
|
245
|
+
// rejects with every cleanup's own error on `errors`, a store's
|
|
246
|
+
// write — and what it wrote — among them.
|
|
247
|
+
logger.error({ err: loggableError(err) }, "shutdown_cleanup_failed");
|
|
239
248
|
exitCode = 1;
|
|
240
249
|
outcome = "cleanup-failed";
|
|
241
250
|
}
|
|
242
|
-
logger.info({ reason: outcome, drain: reason, exitCode }, "
|
|
251
|
+
logger.info({ reason: outcome, drain: reason, exitCode }, "shutdown_complete");
|
|
243
252
|
exit(exitCode);
|
|
244
253
|
};
|
|
245
254
|
|
|
@@ -247,13 +256,10 @@ export function installGracefulShutdown(server: Server, options: GracefulShutdow
|
|
|
247
256
|
if (shuttingDown) return;
|
|
248
257
|
shuttingDown = true;
|
|
249
258
|
for (const signal of SIGNALS) offSignal(signal, handler);
|
|
250
|
-
logger.info({ drainTimeoutMs }, "
|
|
259
|
+
logger.info({ drainTimeoutMs }, "shutdown_draining");
|
|
251
260
|
|
|
252
261
|
const deadline = setTimeout(() => {
|
|
253
|
-
logger.error(
|
|
254
|
-
{ drainTimeoutMs },
|
|
255
|
-
"graceful shutdown: drain deadline exceeded, closing remaining connections",
|
|
256
|
-
);
|
|
262
|
+
logger.error({ drainTimeoutMs }, "shutdown_drain_deadline_exceeded");
|
|
257
263
|
server.closeAllConnections();
|
|
258
264
|
void finish(1, "drain-timeout");
|
|
259
265
|
}, drainTimeoutMs);
|
|
@@ -269,7 +275,7 @@ export function installGracefulShutdown(server: Server, options: GracefulShutdow
|
|
|
269
275
|
// here. Reporting "drained" and exiting 0 on it would tell an
|
|
270
276
|
// orchestrator the shutdown went cleanly when the listener did
|
|
271
277
|
// not actually come down.
|
|
272
|
-
logger.error({ err }, "
|
|
278
|
+
logger.error({ err: loggableError(err) }, "shutdown_server_close_failed");
|
|
273
279
|
void finish(1, "close-failed");
|
|
274
280
|
return;
|
|
275
281
|
}
|
package/templates/versions.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@o3co/auth-provider-core": "0.
|
|
3
|
-
"@o3co/auth-provider-device-grant": "0.
|
|
4
|
-
"@o3co/auth-provider-dpop": "0.
|
|
5
|
-
"@o3co/auth-provider-mtls": "0.
|
|
6
|
-
"@o3co/auth-provider-federation-apple": "0.
|
|
7
|
-
"@o3co/auth-provider-federation-google": "0.
|
|
8
|
-
"@o3co/auth-provider-federation-github": "0.
|
|
9
|
-
"@o3co/auth-provider-federation-grants": "0.
|
|
10
|
-
"@o3co/auth-provider-federation-oidc": "0.
|
|
11
|
-
"@o3co/auth-provider-oauth": "0.
|
|
12
|
-
"@o3co/auth-provider-oauth-token-exchange": "0.
|
|
13
|
-
"@o3co/auth-provider-session": "0.
|
|
14
|
-
"@o3co/auth-provider-foundation": "0.
|
|
15
|
-
"@o3co/auth-provider-redis": "0.
|
|
16
|
-
"@o3co/auth-provider-webauthn": "0.
|
|
2
|
+
"@o3co/auth-provider-core": "0.16.0",
|
|
3
|
+
"@o3co/auth-provider-device-grant": "0.16.0",
|
|
4
|
+
"@o3co/auth-provider-dpop": "0.16.0",
|
|
5
|
+
"@o3co/auth-provider-mtls": "0.16.0",
|
|
6
|
+
"@o3co/auth-provider-federation-apple": "0.16.0",
|
|
7
|
+
"@o3co/auth-provider-federation-google": "0.16.0",
|
|
8
|
+
"@o3co/auth-provider-federation-github": "0.16.0",
|
|
9
|
+
"@o3co/auth-provider-federation-grants": "0.16.0",
|
|
10
|
+
"@o3co/auth-provider-federation-oidc": "0.16.0",
|
|
11
|
+
"@o3co/auth-provider-oauth": "0.16.0",
|
|
12
|
+
"@o3co/auth-provider-oauth-token-exchange": "0.16.0",
|
|
13
|
+
"@o3co/auth-provider-session": "0.16.0",
|
|
14
|
+
"@o3co/auth-provider-foundation": "0.16.0",
|
|
15
|
+
"@o3co/auth-provider-redis": "0.16.0",
|
|
16
|
+
"@o3co/auth-provider-webauthn": "0.16.0"
|
|
17
17
|
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import type { Logger } from "@o3co/auth-provider-core";
|
|
17
|
-
import express from "express";
|
|
18
|
-
import request from "supertest";
|
|
19
|
-
import { describe, expect, it, vi } from "vitest";
|
|
20
|
-
import { createTerminalErrorHandler } from "../terminalError.mjs";
|
|
21
|
-
|
|
22
|
-
const makeLogger = () => {
|
|
23
|
-
const error = vi.fn();
|
|
24
|
-
const logger = {
|
|
25
|
-
error,
|
|
26
|
-
warn: vi.fn(),
|
|
27
|
-
info: vi.fn(),
|
|
28
|
-
debug: vi.fn(),
|
|
29
|
-
} as unknown as Logger;
|
|
30
|
-
return { logger, error };
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const makeApp = (logger: Logger) => {
|
|
34
|
-
const app = express();
|
|
35
|
-
app.use(express.json());
|
|
36
|
-
app.post("/echo", (_req, res) => {
|
|
37
|
-
res.status(200).json({ ok: true });
|
|
38
|
-
});
|
|
39
|
-
app.get("/boom", () => {
|
|
40
|
-
throw new Error("route exploded");
|
|
41
|
-
});
|
|
42
|
-
app.use(createTerminalErrorHandler(logger));
|
|
43
|
-
return app;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
describe("terminal error handler (#293 item 8)", () => {
|
|
47
|
-
it("answers malformed JSON with the client's 400 in the shared envelope, not Express's HTML page", async () => {
|
|
48
|
-
const { logger, error } = makeLogger();
|
|
49
|
-
const res = await request(makeApp(logger))
|
|
50
|
-
.post("/echo")
|
|
51
|
-
.set("Content-Type", "application/json")
|
|
52
|
-
.send("{not json");
|
|
53
|
-
|
|
54
|
-
expect(res.status).toBe(400);
|
|
55
|
-
expect(res.headers["content-type"]).toContain("application/json");
|
|
56
|
-
expect(res.body.error).toBe("invalid_request");
|
|
57
|
-
// The client's malformed body is not a server error worth an error line.
|
|
58
|
-
expect(error).not.toHaveBeenCalled();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("keeps the body parser's own status for an over-limit body (413)", async () => {
|
|
62
|
-
const { logger } = makeLogger();
|
|
63
|
-
const app = express();
|
|
64
|
-
app.use(express.json({ limit: 16 }));
|
|
65
|
-
app.post("/echo", (_req, res) => {
|
|
66
|
-
res.status(200).json({ ok: true });
|
|
67
|
-
});
|
|
68
|
-
app.use(createTerminalErrorHandler(logger));
|
|
69
|
-
|
|
70
|
-
const res = await request(app)
|
|
71
|
-
.post("/echo")
|
|
72
|
-
.set("Content-Type", "application/json")
|
|
73
|
-
.send(JSON.stringify({ pad: "x".repeat(64) }));
|
|
74
|
-
|
|
75
|
-
expect(res.status).toBe(413);
|
|
76
|
-
expect(res.body.error).toBe("invalid_request");
|
|
77
|
-
expect(res.body.error_description).toBe("request body too large");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("answers an unexpected route throw with 500 server_error and logs it structurally", async () => {
|
|
81
|
-
const { logger, error } = makeLogger();
|
|
82
|
-
const res = await request(makeApp(logger)).get("/boom");
|
|
83
|
-
|
|
84
|
-
expect(res.status).toBe(500);
|
|
85
|
-
expect(res.body).toEqual({
|
|
86
|
-
error: "server_error",
|
|
87
|
-
error_description: "Internal server error",
|
|
88
|
-
});
|
|
89
|
-
// No stack trace on the wire — the envelope is the whole body.
|
|
90
|
-
expect(JSON.stringify(res.body)).not.toContain("route exploded");
|
|
91
|
-
expect(error).toHaveBeenCalledWith(
|
|
92
|
-
expect.objectContaining({ endpoint: "/boom" }),
|
|
93
|
-
"unhandled_request_error",
|
|
94
|
-
);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import { errorEnvelope, type Logger } from "@o3co/auth-provider-core";
|
|
17
|
-
import type { ErrorRequestHandler } from "express";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Terminal Express error handler (#293 item 8).
|
|
21
|
-
*
|
|
22
|
-
* Anything a route threw past its own try/catch — and every body-parser
|
|
23
|
-
* rejection, which fires before any route runs — used to fall through to
|
|
24
|
-
* Express's default handler: an HTML 500 outside the structured-log pipeline,
|
|
25
|
-
* carrying a stack trace outside production. A client that parses only the
|
|
26
|
-
* RFC 6749 §5.2 JSON envelope this surface answers with everywhere else has
|
|
27
|
-
* no way to read that page.
|
|
28
|
-
*
|
|
29
|
-
* Body-parser failures (malformed JSON/form, over-limit, bad charset) carry
|
|
30
|
-
* their own 4xx `status` and are the client's fault: keep the status, wrap it
|
|
31
|
-
* in the shared envelope, and do not log them as server errors. Everything
|
|
32
|
-
* else is logged with the request path and answered `500 server_error`.
|
|
33
|
-
*
|
|
34
|
-
* Mount it AFTER every route (`app.use(handle.router)` included) — Express
|
|
35
|
-
* routes errors only to handlers registered later.
|
|
36
|
-
*/
|
|
37
|
-
export const createTerminalErrorHandler = (logger: Logger): ErrorRequestHandler => {
|
|
38
|
-
return (err, req, res, next) => {
|
|
39
|
-
// A failure after the response started is not ours to rewrite; handing
|
|
40
|
-
// it back lets Express close the connection.
|
|
41
|
-
if (res.headersSent) {
|
|
42
|
-
next(err);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const status = (err as { status?: unknown; statusCode?: unknown } | null) ?? {};
|
|
46
|
-
const httpStatus =
|
|
47
|
-
typeof status.status === "number"
|
|
48
|
-
? status.status
|
|
49
|
-
: typeof status.statusCode === "number"
|
|
50
|
-
? status.statusCode
|
|
51
|
-
: undefined;
|
|
52
|
-
if (httpStatus !== undefined && httpStatus >= 400 && httpStatus < 500) {
|
|
53
|
-
const description = httpStatus === 413 ? "request body too large" : "malformed request body";
|
|
54
|
-
res.status(httpStatus).json(errorEnvelope("invalid_request", description));
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
logger.error({ err, endpoint: req.path }, "unhandled_request_error");
|
|
58
|
-
res.status(500).json(errorEnvelope("server_error", "Internal server error"));
|
|
59
|
-
};
|
|
60
|
-
};
|