This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { I18nService, I18nContext } from 'nestjs-i18n';
|
||||
import { ApiResponse, createErrorResponse } from '../types/api-response.type';
|
||||
|
||||
/**
|
||||
* Global exception filter that catches all exceptions
|
||||
* and returns a standardized ApiResponse with HTTP 200
|
||||
*/
|
||||
@Catch()
|
||||
export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(GlobalExceptionFilter.name);
|
||||
|
||||
constructor(private readonly i18n?: I18nService) {}
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
// Determine status and message
|
||||
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let message = 'Internal server error';
|
||||
let errors: string[] = [];
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
if (typeof exceptionResponse === 'string') {
|
||||
message = exceptionResponse;
|
||||
} else if (typeof exceptionResponse === 'object') {
|
||||
const responseObj = exceptionResponse as Record<string, unknown>;
|
||||
message = (responseObj.message as string) || exception.message;
|
||||
|
||||
// Handle validation errors (class-validator)
|
||||
if (Array.isArray(responseObj.message)) {
|
||||
errors = responseObj.message as string[];
|
||||
message = 'VALIDATION_FAILED';
|
||||
}
|
||||
}
|
||||
} else if (exception instanceof Error) {
|
||||
message = exception.message;
|
||||
}
|
||||
|
||||
// Try to translate the message
|
||||
if (this.i18n) {
|
||||
try {
|
||||
const i18nContext = I18nContext.current();
|
||||
let lang = i18nContext?.lang;
|
||||
|
||||
if (!lang) {
|
||||
const acceptLanguage = request.headers['accept-language'];
|
||||
const xLang = request.headers['x-lang'];
|
||||
|
||||
if (xLang) {
|
||||
lang = Array.isArray(xLang) ? xLang[0] : xLang;
|
||||
} else if (acceptLanguage) {
|
||||
// Take first preferred language: "tr-TR,en;q=0.9" -> "tr"
|
||||
lang = acceptLanguage.split(',')[0].split(';')[0].split('-')[0];
|
||||
}
|
||||
}
|
||||
|
||||
lang = lang || 'en';
|
||||
|
||||
// Translate validation error specially
|
||||
if (message === 'VALIDATION_FAILED') {
|
||||
message = this.i18n.translate('errors.VALIDATION_FAILED', { lang });
|
||||
} else {
|
||||
// Try dynamic translation
|
||||
const translatedMessage = this.i18n.translate(`errors.${message}`, {
|
||||
lang,
|
||||
});
|
||||
// Only update if translation exists (key is different from result)
|
||||
if (translatedMessage !== `errors.${message}`) {
|
||||
message = translatedMessage;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Keep original message if translation fails
|
||||
}
|
||||
}
|
||||
|
||||
// Log the error
|
||||
this.logger.error(
|
||||
`${request.method} ${request.url} - ${status} - ${message}`,
|
||||
exception instanceof Error ? exception.stack : undefined,
|
||||
);
|
||||
|
||||
// Build response
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
const errorResponse: ApiResponse<null> = createErrorResponse(
|
||||
message,
|
||||
status,
|
||||
errors,
|
||||
isDevelopment && exception instanceof Error ? exception.stack : undefined,
|
||||
);
|
||||
|
||||
// Always return HTTP 200, actual status in body
|
||||
response.status(200).json(errorResponse);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user