@ogcio/fastify-error-handler 4.0.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.md ADDED
@@ -0,0 +1,33 @@
1
+ # Fastify Error Handler
2
+
3
+ This error handler package's goal is to standardize the management of the errors across Fastify services.
4
+
5
+ ## How to use it
6
+
7
+ To use this package 2 steps are needed:
8
+ - install the package with
9
+ ```
10
+ npm i @ogcio/fastify-error-handler
11
+ ```
12
+
13
+ - use the `initializeErrorHandler(server)` method to set the error handlers for the `fastify` server
14
+
15
+ That's it!
16
+
17
+ ## How to raise errors
18
+
19
+ To standardize error handling in the Fastify services, the suggested way to go is to raise exceptions by using the `HttpError` errors from the [fastify-sensible](https://github.com/fastify/fastify-sensible) package.
20
+
21
+ It exposes some predefined error types
22
+
23
+ ```
24
+ import { httpErrors } from "@fastify/sensible";
25
+ ......
26
+ // predefined error
27
+ throw httpErrors.notFound("Route not found");
28
+
29
+ // custom error
30
+ throw httpErrors.createError(404, "message", {additional: {data: "here"}});
31
+ ```
32
+
33
+ The `error-handler` package is ready to manage the errors, log them and transform them in the output HTTP error you need.
@@ -0,0 +1,88 @@
1
+ import { FastifyError, createError } from "@fastify/error";
2
+ import { pino, DestinationStream } from "pino";
3
+ import fastify, { FastifyInstance } from "fastify";
4
+ import { initializeErrorHandler } from "../../src/index.js";
5
+ import fastifySensible from "@fastify/sensible";
6
+ import httpErrors from "http-errors";
7
+ export const buildFastify = async (
8
+ loggerDestination?: DestinationStream
9
+ ): Promise<FastifyInstance> => {
10
+ const server = fastify({ logger: pino({}, loggerDestination) });
11
+ initializeErrorHandler(server as unknown as FastifyInstance);
12
+ await server.register(fastifySensible);
13
+ server.get("/error", async (request, _reply) => {
14
+ const parsed = request.query as { [x: string]: unknown };
15
+ const requestedStatusCode = Number(parsed["status_code"] ?? "500");
16
+ const requestedMessage = String(parsed["error_message"] ?? "WHOOOPS");
17
+
18
+ if (!parsed["status_code"]) {
19
+ throw new Error(requestedMessage);
20
+ }
21
+
22
+ throw createError(
23
+ "CUSTOM_CODE",
24
+ requestedMessage as string,
25
+ requestedStatusCode as number
26
+ )();
27
+ });
28
+
29
+ server.get("/validation", async (request, _reply) => {
30
+ const parsed = request.query as { [x: string]: unknown };
31
+ const requestedMessage = String(parsed["error_message"] ?? "WHOOOPS");
32
+
33
+ const error = createError(
34
+ "CUSTOM_CODE",
35
+ requestedMessage as string,
36
+ 422,
37
+ )() as FastifyError & {
38
+ headers: { [x: string]: unknown };
39
+ status: number | undefined;
40
+ };
41
+
42
+ error.validation = [
43
+ {
44
+ keyword: "field",
45
+ instancePath: "the.instance.path",
46
+ schemaPath: "the.schema.path",
47
+ params: { field: "one", property: "two" },
48
+ message: requestedMessage,
49
+ },
50
+ ];
51
+ error.validationContext = "body";
52
+ error.status = 423;
53
+
54
+ throw error;
55
+ });
56
+
57
+ server.get("/life-events/custom", async (request, _reply) => {
58
+ const parsed = request.query as { [x: string]: unknown };
59
+ const requestedStatusCode = Number(parsed["status_code"] ?? "500");
60
+
61
+ throw server.httpErrors.createError(
62
+ requestedStatusCode as number,
63
+ "message"
64
+ );
65
+ });
66
+
67
+ server.get("/life-events/validation", async (_request, _reply) => {
68
+ throw server.httpErrors.createError(422, "message", {
69
+ validationErrors: [
70
+ { fieldName: "field", message: "error", validationRule: "equal" },
71
+ ],
72
+ });
73
+ });
74
+
75
+ server.get("/life-events/:errorCode", async (request, _reply) => {
76
+ const errorCode = Number((request.params! as { errorCode: string })
77
+ .errorCode);
78
+ if (!httpErrors[errorCode]) {
79
+ throw new Error("Wrong parameter");
80
+ }
81
+
82
+ const errorObj = httpErrors[errorCode];
83
+
84
+ throw new errorObj("Failed Correctly!");
85
+ });
86
+
87
+ return server as unknown as FastifyInstance;
88
+ };
@@ -0,0 +1,44 @@
1
+ import { FastifyInstance } from "fastify";
2
+ import { buildFastify } from "./build-fastify.js";
3
+ export const DEFAULT_HOSTNAME = "localhost:80";
4
+ export const DEFAULT_USER_AGENT = "lightMyRequest";
5
+ export const DEFAULT_REQUEST_HEADERS = {
6
+ "user-agent": "lightMyRequest",
7
+ host: "localhost:80",
8
+ };
9
+ export const DEFAULT_CLIENT_IP = "127.0.0.1";
10
+ export const DEFAULT_CONTENT_TYPE = "application/json; charset=utf-8";
11
+ export const DEFAULT_METHOD = "GET";
12
+ export const DEFAULT_SCHEME = "http";
13
+ export const DEFAULT_PATH = "/error";
14
+
15
+ export interface TestingLoggerDestination {
16
+ loggerDestination: {
17
+ write: (_data: string) => number;
18
+ };
19
+ getLoggedRecords: () => string[];
20
+ }
21
+
22
+ export const getTestingDestinationLogger = (): TestingLoggerDestination => {
23
+ const testCaseRecords: string[] = [];
24
+ const getLoggedRecords = () => testCaseRecords;
25
+ const loggerDestination = {
26
+ write: (data: string) => testCaseRecords.push(data),
27
+ };
28
+
29
+ return { loggerDestination, getLoggedRecords };
30
+ };
31
+
32
+ export const initializeServer = async (): Promise<{
33
+ server: FastifyInstance;
34
+ loggingDestination: TestingLoggerDestination;
35
+ }> => {
36
+ const loggingDestination = getTestingDestinationLogger();
37
+ const server = await buildFastify(loggingDestination.loggerDestination);
38
+
39
+ return { server, loggingDestination };
40
+ };
41
+
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ export const parseLogEntry = (logEntry: string): { [x: string]: any } =>
44
+ JSON.parse(logEntry);
@@ -0,0 +1,99 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ DEFAULT_METHOD,
5
+ DEFAULT_PATH,
6
+ initializeServer,
7
+ } from "./helpers/fastify-test-helpers.js";
8
+ import httpErrors from "http-errors";
9
+ import { HttpErrorClasses } from "@ogcio/shared-errors";
10
+
11
+ test("Common error is managed as expected", async (t) => {
12
+ const { server } = await initializeServer();
13
+ t.after(() => server.close());
14
+
15
+ const response = await server.inject({
16
+ method: DEFAULT_METHOD,
17
+ url: DEFAULT_PATH,
18
+ query: { status_code: "500", error_message: "error message" },
19
+ });
20
+
21
+ assert.ok(typeof response !== "undefined");
22
+ assert.equal(response?.statusCode, 500);
23
+ assert.deepStrictEqual(response.json(), {
24
+ code: HttpErrorClasses.ServerError,
25
+ detail: "error message",
26
+ requestId: "req-1",
27
+ name: "FastifyError",
28
+ });
29
+ });
30
+
31
+ test("Validation error is managed as a Life Events One", async (t) => {
32
+ const { server } = await initializeServer();
33
+ t.after(() => server.close());
34
+
35
+ const response = await server.inject({
36
+ method: DEFAULT_METHOD,
37
+ url: "/validation",
38
+ query: { error_message: "error message" },
39
+ });
40
+
41
+ assert.ok(typeof response !== "undefined");
42
+ assert.equal(response?.statusCode, 422);
43
+ assert.deepStrictEqual(response.json(), {
44
+ code: HttpErrorClasses.ValidationError,
45
+ detail: "error message",
46
+ requestId: "req-1",
47
+ name: "UnprocessableEntityError",
48
+ validation: [
49
+ {
50
+ fieldName: "the.instance.path",
51
+ message: "error message",
52
+ validationRule: "field",
53
+ additionalInfo: {
54
+ field: "one",
55
+ property: "two",
56
+ },
57
+ },
58
+ ],
59
+ });
60
+ });
61
+
62
+ test("If an error with status 200 is raised, it is managed as an unknown one", async (t) => {
63
+ const { server } = await initializeServer();
64
+ t.after(() => server.close());
65
+
66
+ const response = await server.inject({
67
+ method: DEFAULT_METHOD,
68
+ url: DEFAULT_PATH,
69
+ query: { status_code: "200", error_message: "error message" },
70
+ });
71
+
72
+ assert.ok(typeof response !== "undefined");
73
+ assert.equal(response?.statusCode, 500);
74
+ assert.deepStrictEqual(response.json(), {
75
+ code: HttpErrorClasses.UnknownError,
76
+ detail: "error message",
77
+ requestId: "req-1",
78
+ name: "FastifyError",
79
+ });
80
+ });
81
+
82
+ test("404 error is managed as expected", async (t) => {
83
+ const { server } = await initializeServer();
84
+ t.after(() => server.close());
85
+
86
+ const response = await server.inject({
87
+ method: DEFAULT_METHOD,
88
+ url: "/this-path-does-not-exist",
89
+ });
90
+
91
+ assert.ok(typeof response !== "undefined");
92
+ assert.equal(response?.statusCode, 404);
93
+ assert.deepStrictEqual(response.json(), {
94
+ code: HttpErrorClasses.NotFoundError,
95
+ detail: "Route not found: /this-path-does-not-exist",
96
+ requestId: "req-1",
97
+ name: new httpErrors[404]("TEMP").name,
98
+ });
99
+ });
@@ -0,0 +1,82 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ DEFAULT_METHOD,
5
+ initializeServer,
6
+ } from "./helpers/fastify-test-helpers.js";
7
+ import httpErrors from "http-errors";
8
+ import * as sharedErrors from "@ogcio/shared-errors";
9
+
10
+ const errorsProvider = [
11
+ { errorType: httpErrors[401], expectedStatusCode: 401 },
12
+ { errorType: httpErrors[403], expectedStatusCode: 403 },
13
+ { errorType: httpErrors[500], expectedStatusCode: 500 },
14
+ { errorType: httpErrors[404], expectedStatusCode: 404 },
15
+ { errorType: httpErrors[500], expectedStatusCode: 500 },
16
+ { errorType: httpErrors[502], expectedStatusCode: 502 },
17
+ ];
18
+
19
+ errorsProvider.forEach((errorProv) =>
20
+ test(`Error is managed in the correct way - ${errorProv.errorType.name}`, async (t) => {
21
+ const { server } = await initializeServer();
22
+ t.after(() => server.close());
23
+
24
+ const errorInstance = new errorProv.errorType("message");
25
+
26
+ const response = await server.inject({
27
+ method: DEFAULT_METHOD,
28
+ url: `/life-events/${errorProv.expectedStatusCode}`,
29
+ });
30
+
31
+ assert.ok(typeof response !== "undefined");
32
+ assert.equal(response?.statusCode, errorProv.expectedStatusCode);
33
+ assert.deepStrictEqual(response.json(), {
34
+ code: sharedErrors.parseHttpErrorClass(errorProv.expectedStatusCode),
35
+ detail: "Failed Correctly!",
36
+ requestId: "req-1",
37
+ name: errorInstance.name,
38
+ });
39
+ })
40
+ );
41
+
42
+ test(`Custom error is managed based on parameters`, async (t) => {
43
+ const { server } = await initializeServer();
44
+ t.after(() => server.close());
45
+
46
+ const response = await server.inject({
47
+ method: DEFAULT_METHOD,
48
+ url: `/life-events/custom`,
49
+ query: { status_code: "503" },
50
+ });
51
+
52
+ assert.ok(typeof response !== "undefined");
53
+ assert.equal(response?.statusCode, 503);
54
+ assert.deepStrictEqual(response.json(), {
55
+ code: sharedErrors.parseHttpErrorClass(503),
56
+ detail: "message",
57
+ requestId: "req-1",
58
+ name: new httpErrors[503]("MOCK").name,
59
+ });
60
+ });
61
+
62
+ test(`Validation error is managed as expected`, async (t) => {
63
+ const { server } = await initializeServer();
64
+ t.after(() => server.close());
65
+
66
+ const response = await server.inject({
67
+ method: DEFAULT_METHOD,
68
+ url: `/life-events/validation`,
69
+ });
70
+
71
+ assert.ok(typeof response !== "undefined");
72
+ assert.equal(response?.statusCode, 422);
73
+ assert.deepStrictEqual(response.json(), {
74
+ code: sharedErrors.parseHttpErrorClass(422),
75
+ detail: "message",
76
+ requestId: "req-1",
77
+ name: new httpErrors[422]("MOCK").name,
78
+ validation: [
79
+ { fieldName: "field", message: "error", validationRule: "equal" },
80
+ ],
81
+ });
82
+ });
@@ -0,0 +1,3 @@
1
+ import { FastifyInstance } from "fastify";
2
+ export declare const initializeErrorHandler: (server: FastifyInstance) => void;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAM1C,eAAO,MAAM,sBAAsB,WAAY,eAAe,KAAG,IAGhE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { initializeNotFoundHandler, setupErrorHandler } from "./initialize-error-handler.js";
2
+ export const initializeErrorHandler = (server) => {
3
+ setupErrorHandler(server);
4
+ initializeNotFoundHandler(server);
5
+ };
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,+BAA+B,CAAC;AAEvC,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,MAAuB,EAAQ,EAAE;IACtE,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,yBAAyB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { FastifyInstance } from "fastify";
2
+ import { HttpErrorClasses, ValidationErrorData } from "@ogcio/shared-errors";
3
+ export interface OutputHttpError {
4
+ code: HttpErrorClasses;
5
+ detail: string;
6
+ requestId: string;
7
+ name: string;
8
+ validation?: ValidationErrorData[];
9
+ process?: string;
10
+ }
11
+ export declare const setupErrorHandler: (server: FastifyInstance) => void;
12
+ export declare const initializeNotFoundHandler: (server: FastifyInstance) => void;
13
+ //# sourceMappingURL=initialize-error-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"initialize-error-handler.d.ts","sourceRoot":"","sources":["../src/initialize-error-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,eAAe,EAEhB,MAAM,SAAS,CAAC;AAOjB,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EAEpB,MAAM,sBAAsB,CAAC;AAI9B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,iBAAiB,WAAY,eAAe,KAAG,IAyC3D,CAAC;AAIF,eAAO,MAAM,yBAAyB,WAAY,eAAe,KAAG,IAUnE,CAAC"}
@@ -0,0 +1,118 @@
1
+ import { setLoggingContext, getLoggingContextError, LogMessages, } from "@ogcio/fastify-logging-wrapper";
2
+ import { parseHttpErrorClass, } from "@ogcio/shared-errors";
3
+ import { isHttpError } from "http-errors";
4
+ import { httpErrors } from "@fastify/sensible";
5
+ // The error handler below is the same as the original one in Fastify,
6
+ // just without unwanted log entries
7
+ // I've opened an issue to fastify to ask them if we could avoid logging
8
+ // those entries when disableRequestLogging is true
9
+ // https://github.com/fastify/fastify/issues/5409
10
+ export const setupErrorHandler = (server) => {
11
+ const setErrorHeaders = (error, reply) => {
12
+ const res = reply.raw;
13
+ let statusCode = res.statusCode;
14
+ statusCode = statusCode >= 400 ? statusCode : 500;
15
+ // treat undefined and null as same
16
+ if (error != null) {
17
+ if (error.headers !== undefined) {
18
+ reply.headers(error.headers);
19
+ }
20
+ if (error.status && error.status >= 400) {
21
+ statusCode = error.status;
22
+ }
23
+ else if (error.statusCode && error.statusCode >= 400) {
24
+ statusCode = error.statusCode;
25
+ }
26
+ }
27
+ res.statusCode = statusCode;
28
+ reply.statusCode = res.statusCode;
29
+ };
30
+ server.setErrorHandler(function (error, request, reply) {
31
+ if (isHttpError(error)) {
32
+ manageHttpError(error, request, reply);
33
+ return;
34
+ }
35
+ if (error.validation) {
36
+ const httpError = toOutputHttpValidationError(error);
37
+ manageHttpError(httpError, request, reply);
38
+ return;
39
+ }
40
+ setErrorHeaders(error, reply);
41
+ reply.send(getResponseFromFastifyError(error, request));
42
+ });
43
+ };
44
+ // The error handler below is the same as the original one in Fastify,
45
+ // just without unwanted log entries
46
+ export const initializeNotFoundHandler = (server) => {
47
+ server.setNotFoundHandler((request, reply) => {
48
+ const error = httpErrors.notFound(`Route not found: ${request.url}`);
49
+ setLoggingContext({
50
+ error,
51
+ });
52
+ request.log.error({ error: getLoggingContextError() }, LogMessages.Error);
53
+ manageHttpError(error, request, reply);
54
+ });
55
+ };
56
+ const getValidationFromFastifyError = (validationInput) => {
57
+ const output = [];
58
+ for (const input of validationInput) {
59
+ const key = input.params?.missingProperty ?? input.instancePath.split("/").pop();
60
+ const message = input.message ?? input.keyword;
61
+ if (key && typeof key === "string") {
62
+ output.push({
63
+ fieldName: key,
64
+ message,
65
+ validationRule: input.keyword,
66
+ additionalInfo: input.params,
67
+ });
68
+ continue;
69
+ }
70
+ output.push({
71
+ fieldName: input.schemaPath,
72
+ message,
73
+ validationRule: input.keyword,
74
+ additionalInfo: input.params,
75
+ });
76
+ }
77
+ return { validation: output };
78
+ };
79
+ const getResponseFromFastifyError = (error, request) => {
80
+ const output = {
81
+ code: parseHttpErrorClass(error.statusCode),
82
+ detail: error.message,
83
+ requestId: request.id,
84
+ name: error.name,
85
+ };
86
+ if (error.validation && error.validation.length > 0) {
87
+ output.validation = getValidationFromFastifyError(error.validation).validation;
88
+ }
89
+ return output;
90
+ };
91
+ const manageHttpError = (error, request, reply) => {
92
+ reply.raw.statusCode = error.statusCode;
93
+ reply.statusCode = error.statusCode;
94
+ const errorResponse = {
95
+ code: parseHttpErrorClass(error.statusCode),
96
+ detail: error.message,
97
+ requestId: request.id,
98
+ name: error.name,
99
+ process: error.errorProcess,
100
+ };
101
+ let validationErrors = error.validationErrors && error.validationErrors.length > 0
102
+ ? error.validationErrors
103
+ : undefined;
104
+ if (!validationErrors && error.validation && error.validation.length > 0) {
105
+ validationErrors = error.validation;
106
+ }
107
+ if (validationErrors) {
108
+ errorResponse.validation = validationErrors;
109
+ }
110
+ reply.status(error.statusCode).send(errorResponse);
111
+ };
112
+ const toOutputHttpValidationError = (error) => {
113
+ if (!error.validation) {
114
+ throw httpErrors.createError(500, "This is not a validation error");
115
+ }
116
+ return httpErrors.createError(422, error.message, getValidationFromFastifyError(error.validation));
117
+ };
118
+ //# sourceMappingURL=initialize-error-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"initialize-error-handler.js","sourceRoot":"","sources":["../src/initialize-error-handler.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@ogcio/fastify-error-handler",
3
+ "version": "4.0.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "rm -rf dist tsconfig.prod.tsbuildinfo tsconfig.tsbuildinfo && tsc -p tsconfig.prod.json",
9
+ "test": "tap --jobs=1 --allow-incomplete-coverage __tests__/**/*.test.ts"
10
+ },
11
+ "keywords": [],
12
+ "author": {
13
+ "name": "Samuele Salvatico",
14
+ "email": "samuele.salvatico@nearform.com"
15
+ },
16
+ "license": "ISC",
17
+ "description": "Normalize the error handling of errors with related logs",
18
+ "dependencies": {
19
+ "@fastify/sensible": "^5.6.0",
20
+ "@ogcio/fastify-logging-wrapper": "^4.0.0",
21
+ "fastify": "^4.28.1"
22
+ },
23
+ "devDependencies": {
24
+ "@types/http-errors": "^2.0.4"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/ogcio/shared-node-utils.git",
29
+ "directory": "packages/fastify-error-handler"
30
+ }
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { FastifyInstance } from "fastify";
2
+ import {
3
+ initializeNotFoundHandler,
4
+ setupErrorHandler
5
+ } from "./initialize-error-handler.js";
6
+
7
+ export const initializeErrorHandler = (server: FastifyInstance): void => {
8
+ setupErrorHandler(server);
9
+ initializeNotFoundHandler(server);
10
+ };
@@ -0,0 +1,180 @@
1
+ import {
2
+ FastifyError,
3
+ FastifyRequest,
4
+ FastifyInstance,
5
+ FastifyReply,
6
+ } from "fastify";
7
+ import { FastifySchemaValidationError } from "fastify/types/schema.js";
8
+ import {
9
+ setLoggingContext,
10
+ getLoggingContextError,
11
+ LogMessages,
12
+ } from "@ogcio/fastify-logging-wrapper";
13
+ import {
14
+ HttpErrorClasses,
15
+ ValidationErrorData,
16
+ parseHttpErrorClass,
17
+ } from "@ogcio/shared-errors";
18
+ import { isHttpError } from "http-errors";
19
+ import { HttpError, httpErrors } from "@fastify/sensible";
20
+
21
+ export interface OutputHttpError {
22
+ code: HttpErrorClasses;
23
+ detail: string;
24
+ requestId: string;
25
+ name: string;
26
+ validation?: ValidationErrorData[];
27
+ process?: string;
28
+ }
29
+
30
+ // The error handler below is the same as the original one in Fastify,
31
+ // just without unwanted log entries
32
+ // I've opened an issue to fastify to ask them if we could avoid logging
33
+ // those entries when disableRequestLogging is true
34
+ // https://github.com/fastify/fastify/issues/5409
35
+ export const setupErrorHandler = (server: FastifyInstance): void => {
36
+ const setErrorHeaders = (
37
+ error: null | {
38
+ headers?: { [x: string]: string | number | string[] | undefined };
39
+ status?: number;
40
+ statusCode?: number;
41
+ },
42
+ reply: FastifyReply
43
+ ) => {
44
+ const res = reply.raw;
45
+ let statusCode = res.statusCode;
46
+ statusCode = statusCode >= 400 ? statusCode : 500;
47
+ // treat undefined and null as same
48
+ if (error != null) {
49
+ if (error.headers !== undefined) {
50
+ reply.headers(error.headers);
51
+ }
52
+ if (error.status && error.status >= 400) {
53
+ statusCode = error.status;
54
+ } else if (error.statusCode && error.statusCode >= 400) {
55
+ statusCode = error.statusCode;
56
+ }
57
+ }
58
+ res.statusCode = statusCode;
59
+ reply.statusCode = res.statusCode;
60
+ };
61
+
62
+ server.setErrorHandler(function (error, request, reply) {
63
+ if (isHttpError(error)) {
64
+ manageHttpError(error, request, reply);
65
+ return;
66
+ }
67
+ if (error.validation) {
68
+ const httpError = toOutputHttpValidationError(error);
69
+ manageHttpError(httpError, request, reply);
70
+ return;
71
+ }
72
+
73
+ setErrorHeaders(error, reply);
74
+ reply.send(getResponseFromFastifyError(error, request));
75
+ });
76
+ };
77
+
78
+ // The error handler below is the same as the original one in Fastify,
79
+ // just without unwanted log entries
80
+ export const initializeNotFoundHandler = (server: FastifyInstance): void => {
81
+ server.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => {
82
+ const error = httpErrors.notFound(`Route not found: ${request.url}`);
83
+ setLoggingContext({
84
+ error,
85
+ });
86
+
87
+ request.log.error({ error: getLoggingContextError() }, LogMessages.Error);
88
+ manageHttpError(error, request, reply);
89
+ });
90
+ };
91
+
92
+ const getValidationFromFastifyError = (
93
+ validationInput: FastifySchemaValidationError[]
94
+ ): { validation: ValidationErrorData[] } => {
95
+ const output: ValidationErrorData[] = [];
96
+
97
+ for (const input of validationInput) {
98
+ const key =
99
+ input.params?.missingProperty ?? input.instancePath.split("/").pop();
100
+ const message = input.message ?? input.keyword;
101
+ if (key && typeof key === "string") {
102
+ output.push({
103
+ fieldName: key,
104
+ message,
105
+ validationRule: input.keyword,
106
+ additionalInfo: input.params,
107
+ });
108
+ continue;
109
+ }
110
+
111
+ output.push({
112
+ fieldName: input.schemaPath,
113
+ message,
114
+ validationRule: input.keyword,
115
+ additionalInfo: input.params,
116
+ });
117
+ }
118
+
119
+ return { validation: output };
120
+ };
121
+
122
+ const getResponseFromFastifyError = (
123
+ error: FastifyError,
124
+ request: FastifyRequest
125
+ ): OutputHttpError => {
126
+ const output: OutputHttpError = {
127
+ code: parseHttpErrorClass(error.statusCode),
128
+ detail: error.message,
129
+ requestId: request.id,
130
+ name: error.name,
131
+ };
132
+ if (error.validation && error.validation.length > 0) {
133
+ output.validation = getValidationFromFastifyError(
134
+ error.validation
135
+ ).validation;
136
+ }
137
+
138
+ return output;
139
+ };
140
+
141
+ const manageHttpError = (
142
+ error: HttpError,
143
+ request: FastifyRequest,
144
+ reply: FastifyReply
145
+ ): void => {
146
+ reply.raw.statusCode = error.statusCode;
147
+ reply.statusCode = error.statusCode;
148
+ const errorResponse: OutputHttpError = {
149
+ code: parseHttpErrorClass(error.statusCode),
150
+ detail: error.message,
151
+ requestId: request.id,
152
+ name: error.name,
153
+ process: error.errorProcess,
154
+ };
155
+ let validationErrors =
156
+ error.validationErrors && error.validationErrors.length > 0
157
+ ? error.validationErrors
158
+ : undefined;
159
+ if (!validationErrors && error.validation && error.validation.length > 0) {
160
+ validationErrors = error.validation;
161
+ }
162
+
163
+ if (validationErrors) {
164
+ errorResponse.validation = validationErrors;
165
+ }
166
+
167
+ reply.status(error.statusCode).send(errorResponse);
168
+ };
169
+
170
+ const toOutputHttpValidationError = (error: FastifyError): HttpError => {
171
+ if (!error.validation) {
172
+ throw httpErrors.createError(500, "This is not a validation error");
173
+ }
174
+
175
+ return httpErrors.createError(
176
+ 422,
177
+ error.message,
178
+ getValidationFromFastifyError(error.validation)
179
+ );
180
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "outDir": "dist",
6
+ "rootDir": "src"
7
+ },
8
+ "include": ["src"],
9
+ "references": [
10
+ { "path": "../shared-errors" }
11
+ ]
12
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["__tests__", "dist"]
4
+ }