From 3e55b61872cce81cf12e57c2baa668af57836438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Johanson?= Date: Thu, 19 Dec 2024 13:02:59 +0100 Subject: [PATCH] Add final exception handler to prevent container crashes (#629) --- .../src/filters/global-exception.filter.ts | 62 +++++++++++++++++++ teammapper-backend/src/main.ts | 17 +++++ 2 files changed, 79 insertions(+) create mode 100644 teammapper-backend/src/filters/global-exception.filter.ts diff --git a/teammapper-backend/src/filters/global-exception.filter.ts b/teammapper-backend/src/filters/global-exception.filter.ts new file mode 100644 index 00000000..2ac3e5f4 --- /dev/null +++ b/teammapper-backend/src/filters/global-exception.filter.ts @@ -0,0 +1,62 @@ +import { ExceptionFilter, Catch, ArgumentsHost, Logger, HttpException } from '@nestjs/common'; + +// This is for any unhandled gateway and "internal" NestJS related errors - like if the gateway can't reach clients or things like that. +// It will try to always keep clients and their websockets alive and gracefully send errors over the wire, without revealing internal error reasons. +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(GlobalExceptionFilter.name); + + catch(exception: Error | HttpException | unknown, host: ArgumentsHost) { + const ctx = host.getType(); + + this.logger.error({ + error: exception, + type: exception?.constructor?.name || typeof exception, + message: exception?.message || 'Unknown error', + stack: exception?.stack, + context: ctx, + }); + + try { + switch (ctx) { + case 'http': { + const response = host.switchToHttp().getResponse(); + return response.status(500).json({ + statusCode: 500, + message: 'Internal server error', + timestamp: new Date().toISOString(), + }); + } + + case 'ws': { + const client = host.switchToWs().getClient(); + const error = { + event: 'error', + data: { + message: 'Internal server error', + timestamp: new Date().toISOString(), + }, + }; + + if (typeof client.emit === 'function') { + client.emit('error', error); + } else if (typeof client.send === 'function') { + client.send(JSON.stringify(error)); + } + break; + } + + default: { + // Handle any runtime errors outside HTTP/WS contexts + this.logger.error(`Unhandled exception type: ${ctx}`); + // Emit to process handler as last resort + process.emit('uncaughtException', exception); + } + } + } catch (handlerError) { + // If the error handler itself fails, log it and emit to process + this.logger.error('Global exception handler failed: ', handlerError); + process.emit('uncaughtException', exception); + } + } +} \ No newline at end of file diff --git a/teammapper-backend/src/main.ts b/teammapper-backend/src/main.ts index 861a1e65..f2097df7 100644 --- a/teammapper-backend/src/main.ts +++ b/teammapper-backend/src/main.ts @@ -2,12 +2,29 @@ import { NestFactory } from '@nestjs/core' import AppModule from './app.module' import configService from './config.service' import { createProxyMiddleware } from 'http-proxy-middleware' +import { GlobalExceptionFilter } from './filters/global-exception.filter'; +import { Logger } from '@nestjs/common' async function bootstrap() { + const logger = new Logger('Main Process'); + + // Process-level handlers for uncaught errors - anything that happens outside of NestJS, such as type errors. + // This is only logged server-side so we log the whole stack for better review. + process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception: ', error.stack); + }); + + process.on('unhandledRejection', (reason: unknown) => { + const stack = reason instanceof Error ? reason.stack : 'No stack trace available'; + logger.error('Unhandled Rejection. Stack trace: ', stack); + }); + const app = await NestFactory.create(AppModule, { logger: ['log', 'error', 'warn', 'debug'], }) + app.useGlobalFilters(new GlobalExceptionFilter()); + app.use( '/arasaac/api', createProxyMiddleware({