TypeScript · 1837 bytes Raw Blame History
1 import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify';
2 import type { ApiError } from '../../shared/types.js';
3
4 export async function errorHandler(
5 error: FastifyError,
6 request: FastifyRequest,
7 reply: FastifyReply
8 ): Promise<void> {
9 const timestamp = new Date();
10
11 // Log the error
12 request.log.error({
13 error: {
14 message: error.message,
15 stack: error.stack,
16 code: error.code,
17 },
18 request: {
19 method: request.method,
20 url: request.url,
21 headers: request.headers,
22 },
23 }, 'Request error');
24
25 // Determine status code and error message
26 let statusCode = 500;
27 let message = 'Internal server error';
28 let code = error.code || 'INTERNAL_ERROR';
29
30 if (error.statusCode) {
31 statusCode = error.statusCode;
32 }
33
34 // Handle specific error types
35 switch (error.code) {
36 case 'FST_ERR_VALIDATION':
37 statusCode = 400;
38 message = 'Invalid request data';
39 break;
40 case 'FST_JWT_NO_AUTHORIZATION_IN_HEADER':
41 case 'FST_JWT_AUTHORIZATION_TOKEN_EXPIRED':
42 case 'FST_JWT_AUTHORIZATION_TOKEN_INVALID':
43 statusCode = 401;
44 message = 'Authentication required';
45 break;
46 case 'FST_ERR_NOT_FOUND':
47 statusCode = 404;
48 message = 'Resource not found';
49 break;
50 case 'FST_ERR_TOO_LARGE':
51 statusCode = 413;
52 message = 'File too large';
53 break;
54 default:
55 if (error.message && !error.message.includes('Internal')) {
56 message = error.message;
57 }
58 }
59
60 // Don't expose internal error details in production
61 if (process.env.NODE_ENV === 'production' && statusCode === 500) {
62 message = 'Internal server error';
63 }
64
65 const errorResponse: ApiError = {
66 error: code,
67 message,
68 code: statusCode,
69 timestamp,
70 };
71
72 await reply.status(statusCode).send(errorResponse);
73 }