Skip to content

Commit

Permalink
refactor: error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Theo Gravity committed Sep 28, 2024
1 parent aa409a9 commit f865f2f
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 147 deletions.
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# boilerplate-typescript-monorepo
# fastify-starter-turbo-monorepo

## Sept-28-2024

- Breaking: Change the format of `ApiError` to simplify validation errors
- Update the error handler to use the new `ApiError` format
- Package updates
- Add url path to log context

## Sept-21-2024

Expand All @@ -9,7 +16,6 @@
- Add dev build caching for faster builds during dev
- Removed AJV sanitize plugin. In real-world usage, you wouldn't want to sanitize the input to your API immediately, but later on.


## Sept-07-2024

- Added better formatting for logs
Expand Down
16 changes: 8 additions & 8 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
"test:ci": "NODE_OPTIONS=\"--no-deprecation\" IS_TEST=true TESTCONTAINERS_HOST_OVERRIDE=127.0.0.1 vitest --watch=false"
},
"dependencies": {
"@dotenvx/dotenvx": "1.14.1",
"@fastify/cors": "10.0.0",
"@fastify/jwt": "9.0.0",
"@fastify/swagger": "9.0.0",
"@dotenvx/dotenvx": "1.14.2",
"@fastify/cors": "10.0.1",
"@fastify/jwt": "9.0.1",
"@fastify/swagger": "9.1.0",
"@fastify/swagger-ui": "5.0.1",
"@fastify/type-provider-typebox": "5.0.0",
"@internal/backend-errors": "workspace:*",
Expand All @@ -36,10 +36,10 @@
"dotenv": "16.4.5",
"env-var": "7.5.0",
"fastify": "5.0.0",
"fastify-cli": "7.0.0",
"fastify-cli": "7.0.1",
"fastify-plugin": "5.0.1",
"kysely": "0.27.4",
"loglayer": "4.7.0",
"loglayer": "4.8.0",
"nanoid": "5.0.7",
"pg": "8.13.0",
"pino": "9.4.0",
Expand All @@ -49,11 +49,11 @@
"uuid": "10.0.0"
},
"devDependencies": {
"@faker-js/faker": "9.0.1",
"@faker-js/faker": "9.0.3",
"@internal/tsconfig": "workspace:*",
"@testcontainers/postgresql": "10.13.1",
"@turbo/gen": "2.1.2",
"@types/node": "22.5.5",
"@types/node": "22.7.4",
"@types/pg": "8.11.10",
"hash-runner": "2.0.1",
"kysely-ctl": "0.9.0",
Expand Down
40 changes: 32 additions & 8 deletions apps/backend/src/api-lib/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,37 @@ import { ApiError, BackendErrorCodes, createApiError } from "@internal/backend-e
import { IS_PROD } from "../constants";

export function errorHandler(error: any, request, reply) {
if (request.url) {
request.log.withContext({
apiPath: request.url,
});
}

if (error instanceof ApiError) {
error.reqId = request.id;
request.log.errorOnly(error, {
logLevel: error.logLevel,
});

// If the error is not supposed to be logged, don't log it
// Usually doNotLog = true means that the error has already been logged elsewhere
if (!error.doNotLog) {
request.log
.withContext({
errId: error.errId,
reqId: error.reqId,
})
.errorOnly(error, {
logLevel: error.logLevel,
});
}

if (error.isInternalError) {
const e = createApiError({
code: BackendErrorCodes.INTERNAL_SERVER_ERROR,
validation: error.validation,
validationContext: error.validationContext,
causedBy: error,
...(error?.validationError
? {
validationError: error.validationError,
}
: {}),
});

e.errId = error.errId;
Expand All @@ -23,17 +42,22 @@ export function errorHandler(error: any, request, reply) {
} else {
reply.status(error.statusCode).send(IS_PROD ? error.toJSONSafe() : error.toJSON());
}
// https://github.com/fastify/fastify/blob/main/docs/Reference/Errors.md
} else if (error?.code === "FST_ERR_VALIDATION") {
const e = createApiError({
code: BackendErrorCodes.INPUT_VALIDATION_ERROR,
validation: error.validation,
validationContext: error.validationContext,
validationError: {
validation: error.validation,
validationContext: error.validationContext,
message: error.message,
},
causedBy: error,
});

e.reqId = request.id;

reply.status(500).send(IS_PROD ? e.toJSONSafe() : e.toJSON());
reply.status(e.statusCode).send(IS_PROD ? e.toJSONSafe() : e.toJSON());
// https://github.com/fastify/fastify-jwt?tab=readme-ov-file#error-code
} else {
const e = createApiError({
code: BackendErrorCodes.INTERNAL_SERVER_ERROR,
Expand Down
9 changes: 8 additions & 1 deletion apps/backend/src/plugins/context.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ declare module "fastify" {

async function plugin(fastify: FastifyInstance, _opts) {
fastify.addHook("onRequest", async (request) => {
if (request.url) {
// @ts-ignore
request.log = request.log.withContext({
apiPath: request.url,
});
}

request.ctx = new ApiContext({
db,
log: fastify.log as unknown as LogLayer<P.Logger>,
log: request.log as unknown as LogLayer<P.Logger>,
});
});
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@commitlint/cli": "19.5.0",
"@commitlint/config-conventional": "19.5.0",
"@internal/tsconfig": "workspace:*",
"@types/node": "22.5.5",
"@types/node": "22.7.4",
"husky": "9.1.6",
"lint-staged": "15.2.10",
"syncpack": "13.0.0",
Expand Down
73 changes: 36 additions & 37 deletions packages/backend-errors/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import { BackendErrorCodeDefs, type BackendErrorCodes } from "./error-codes";

const stackFilter = (path) => !/backend-errors/.test(path);

export interface ApiValidationError {
validation: ErrorObject[];
validationContext: string;
message: string;
}

export interface ApiErrorParams {
/**
* Error code
Expand All @@ -30,12 +36,9 @@ export interface ApiErrorParams {
metadataSafe?: Record<string, any>;
/**
* AJV-style validation errors
* @see https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/#validation-messages-with-other-validation-libraries
*/
validation?: ErrorObject[];
/**
* Context of the validation errors
*/
validationContext?: string;
validationError?: ApiValidationError;
/**
* Error object.
*/
Expand All @@ -49,6 +52,10 @@ export interface ApiErrorParams {
* Log level to use for the error. Default is "error".
*/
logLevel?: "error" | "warn" | "info" | "debug" | "trace" | "fatal";
/**
* Don't log the error in the error handler as it has been logged elsewhere.
*/
doNotLog?: boolean;
}

export class ApiError extends Error {
Expand Down Expand Up @@ -76,14 +83,6 @@ export class ApiError extends Error {
* Request ID
*/
reqId?: string;
/**
* AJV-style validation errors
*/
validation?: ErrorObject[];
/**
* Context of the validation errors
*/
validationContext?: string;
/**
* Error object
*/
Expand All @@ -97,30 +96,39 @@ export class ApiError extends Error {
* Log level to use for the error. Default is "error".
*/
logLevel?: "error" | "warn" | "info" | "debug" | "trace" | "fatal";
/**
* AJV-style validation errors
* @see https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/#validation-messages-with-other-validation-libraries
*/
validationError?: ApiValidationError;
/**
* Don't log the error in the error handler as it has been logged elsewhere.
*/
doNotLog?: boolean;

constructor({
code,
message,
statusCode,
metadata,
metadataSafe,
validation,
validationContext,
validationError,
causedBy,
isInternalError,
logLevel,
doNotLog,
}: ApiErrorParams) {
super(message);
this.errId = nanoid(12);
this.code = code;
this.statusCode = statusCode;
this.metadata = metadata;
this.metadataSafe = metadataSafe;
this.validation = validation;
this.validationContext = validationContext;
this.validationError = validationError;
this.causedBy = causedBy;
this.isInternalError = isInternalError || false;
this.logLevel = logLevel || "error";
this.doNotLog = doNotLog || false;

if (Error.captureStackTrace) {
Error.captureStackTrace(this, ApiError);
Expand Down Expand Up @@ -169,8 +177,7 @@ export class ApiError extends Error {
statusCode: this.statusCode,
...metadata,
...(causedBy ? { causedBy } : {}),
...(this.validation ? { validation: this.validation } : {}),
...(this.validationContext ? { validationContext: this.validationContext } : {}),
...(this.validationError ? { validationError: this.validationError } : {}),
stack: this.stack,
};
}
Expand All @@ -190,22 +197,14 @@ export class ApiError extends Error {
message: this.message,
statusCode: this.statusCode,
...(this.metadataSafe ? { metadata: this.metadataSafe } : {}),
...(this.validation ? { validation: this.validation } : {}),
...(this.validationContext ? { validationContext: this.validationContext } : {}),
...(this.validationError ? { validationError: this.validationError } : {}),
};
}
}

export type ApiErrorShort = Pick<
ApiErrorParams,
| "metadataSafe"
| "logLevel"
| "isInternalError"
| "code"
| "metadata"
| "validation"
| "validationContext"
| "causedBy"
"doNotLog" | "metadataSafe" | "logLevel" | "isInternalError" | "code" | "metadata" | "validationError" | "causedBy"
> & {
message?: string;
};
Expand All @@ -218,23 +217,23 @@ export function throwApiError({
code,
metadata,
metadataSafe,
validation,
validationContext,
validationError,
message,
causedBy,
isInternalError,
logLevel,
doNotLog,
}: ApiErrorShort) {
throw createApiError({
code,
message,
metadata,
metadataSafe,
validation,
validationContext,
validationError,
causedBy,
isInternalError,
logLevel,
doNotLog,
});
}

Expand All @@ -247,11 +246,11 @@ export function createApiError({
message,
metadata,
metadataSafe,
validation,
validationContext,
validationError,
causedBy,
isInternalError,
logLevel,
doNotLog,
}: ApiErrorShort) {
const { message: predefinedMessage, statusCode } = BackendErrorCodeDefs[code];

Expand All @@ -261,11 +260,11 @@ export function createApiError({
statusCode,
metadata,
metadataSafe,
validation,
validationContext,
validationError,
causedBy,
isInternalError,
logLevel,
doNotLog,
});
}

Expand Down
Loading

0 comments on commit f865f2f

Please sign in to comment.