Skip to content

Commit 62304a2

Browse files
author
caneppelevitor
committed
feat: Add global exception filter for consistent error handling and logging, add request ID tracking for log correlation across requests, add uncaught exception and unhandled rejection handler
1 parent 79c4784 commit 62304a2

File tree

3 files changed

+150
-17
lines changed

3 files changed

+150
-17
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
ExceptionFilter,
3+
Catch,
4+
ArgumentsHost,
5+
HttpException,
6+
HttpStatus,
7+
Logger,
8+
} from "@nestjs/common";
9+
import { Request, Response } from "express";
10+
11+
@Catch()
12+
export class AllExceptionsFilter implements ExceptionFilter {
13+
private readonly logger = new Logger(AllExceptionsFilter.name);
14+
15+
catch(exception: unknown, host: ArgumentsHost) {
16+
const ctx = host.switchToHttp();
17+
const response = ctx.getResponse<Response>();
18+
const request = ctx.getRequest<Request>();
19+
20+
const status =
21+
exception instanceof HttpException
22+
? exception.getStatus()
23+
: HttpStatus.INTERNAL_SERVER_ERROR;
24+
25+
const message =
26+
exception instanceof HttpException
27+
? exception.getResponse()
28+
: exception instanceof Error
29+
? exception.message
30+
: "Internal server error";
31+
32+
const requestId =
33+
(request as any).requestId ||
34+
request.headers["x-request-id"] ||
35+
`${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
36+
37+
const errorContext = {
38+
requestId,
39+
method: request.method,
40+
url: request.url,
41+
ip: request.ip || request.socket?.remoteAddress,
42+
statusCode: status,
43+
};
44+
45+
// Handle "headers already sent" error
46+
if (
47+
exception instanceof Error &&
48+
exception.message.includes("Cannot set headers after they are sent")
49+
) {
50+
this.logger.error(
51+
`Headers already sent - ${request.method} ${request.url} | RequestId: ${requestId}`,
52+
exception.stack
53+
);
54+
return;
55+
}
56+
57+
// Log the error with context
58+
const errorMessage =
59+
typeof message === "string" ? message : JSON.stringify(message);
60+
this.logger.error(
61+
`${request.method} ${request.url} | Status: ${status} | RequestId: ${requestId} | Error: ${errorMessage}`,
62+
exception instanceof Error ? exception.stack : ""
63+
);
64+
65+
// Send error response only if headers haven't been sent
66+
if (!response.headersSent) {
67+
response.status(status).json({
68+
requestId,
69+
statusCode: status,
70+
timestamp: new Date().toISOString(),
71+
path: request.url,
72+
message:
73+
status === HttpStatus.INTERNAL_SERVER_ERROR
74+
? "Internal server error"
75+
: message,
76+
});
77+
}
78+
}
79+
}

server/main.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
77
import loadConfig from "./configLoader";
88
import * as dotenv from "dotenv";
99
import { WinstonLogger } from "./winstonLogger";
10+
import { AllExceptionsFilter } from "./filters/http-exception.filter";
1011
const cookieParser = require("cookie-parser");
1112
const mongoose = require("mongoose");
1213
dotenv.config();
@@ -23,11 +24,40 @@ async function initApp() {
2324
origin: options?.cors || "*",
2425
credentials: true,
2526
methods: "GET,HEAD,PUT,PATCH,POST,DELETE, OPTIONS",
26-
allowedHeaders: ["accept", "x-requested-with", "content-type"],
27+
allowedHeaders: [
28+
"accept",
29+
"x-requested-with",
30+
"content-type",
31+
"x-request-id",
32+
],
33+
exposedHeaders: ["x-request-id"],
2734
};
2835

2936
const logger = new WinstonLogger();
3037

38+
// Handle uncaught exceptions
39+
process.on("uncaughtException", (error: Error) => {
40+
logger.error(
41+
`Uncaught Exception: ${error.message}`,
42+
error.stack,
43+
"UncaughtException"
44+
);
45+
setTimeout(() => process.exit(1), 1000);
46+
});
47+
48+
// Handle unhandled promise rejections
49+
process.on("unhandledRejection", (reason: any) => {
50+
const errorMessage =
51+
reason instanceof Error
52+
? `${reason.message}\n${reason.stack}`
53+
: String(reason);
54+
logger.error(
55+
`Unhandled Rejection: ${errorMessage}`,
56+
"",
57+
"UnhandledRejection"
58+
);
59+
});
60+
3161
const app = await NestFactory.create<NestExpressApplication>(
3262
AppModule.register(options),
3363
{
@@ -58,6 +88,9 @@ async function initApp() {
5888
})
5989
);
6090

91+
// Global exception filter for consistent error handling and logging
92+
app.useGlobalFilters(new AllExceptionsFilter());
93+
6194
// FIXME: not working but we need to enable in the future
6295
// app.use(helmet());
6396
app.use(cookieParser());
Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,45 @@
1-
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
2-
import { Request, Response, NextFunction } from 'express';
1+
import { Injectable, Logger, NestMiddleware } from "@nestjs/common";
2+
import { Request, Response, NextFunction } from "express";
3+
4+
declare global {
5+
namespace Express {
6+
interface Request {
7+
requestId?: string;
8+
startTime?: number;
9+
}
10+
}
11+
}
312

413
@Injectable()
514
export class LoggerMiddleware implements NestMiddleware {
6-
private logger = new Logger('HTTP');
15+
private logger = new Logger("HTTP");
16+
17+
use(request: Request, response: Response, next: NextFunction): void {
18+
const startTime = Date.now();
19+
const { ip, method, originalUrl } = request;
20+
const userAgent = request.get("user-agent") || "";
721

8-
use(request: Request, response: Response, next: NextFunction): void {
9-
const { ip, method, originalUrl } = request;
10-
const userAgent = request.get('user-agent') || '';
22+
// Generate or use existing request ID
23+
const requestId =
24+
(request.headers["x-request-id"] as string) ||
25+
`${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1126

12-
response.on('finish', () => {
13-
const { statusCode } = response;
14-
const contentLength = response.get('content-length');
27+
// Attach to request for use in exception filters and services
28+
request.requestId = requestId;
29+
request.startTime = startTime;
30+
request.headers["x-request-id"] = requestId;
31+
response.setHeader("x-request-id", requestId);
1532

16-
this.logger.log(
17-
`${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
18-
);
19-
});
33+
response.on("finish", () => {
34+
const { statusCode } = response;
35+
const contentLength = response.get("content-length") || 0;
36+
const responseTime = Date.now() - startTime;
2037

21-
next();
22-
}
23-
}
38+
this.logger.log(
39+
`${method} ${originalUrl} ${statusCode} ${contentLength} ${responseTime}ms - ${userAgent} ${ip}`
40+
);
41+
});
2442

43+
next();
44+
}
45+
}

0 commit comments

Comments
 (0)