import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
import type { Request, Response } from 'express';
import { ApiError } from './api-error';
@Catch()
export class ApiExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void {
const ctx=host.switchToHttp();
const request=ctx.getRequest<Request & {correlationId?:string}>();
const response=ctx.getResponse<Response>();
let status=HttpStatus.INTERNAL_SERVER_ERROR;
let code='internal.unexpected_error';
let params:Record<string,unknown>={};
if(exception instanceof ApiError) {
status=exception.statusCode;
code=exception.code;
params=this.safeParams(exception.params);
} else if(this.isHttpException(exception)) {
status=exception.getStatus();
const body:any=exception.getResponse();
if(body && typeof body==='object' && typeof body.code==='string') {
code=body.code;
} else {
code=`http.${status}`;
}
if(status<500 && body && typeof body==='object' && Array.isArray(body.message)) {
params={fields:body.message.slice(0,50)};
}
}
if(status>=500) {
const errorDetails =
process.env.NODE_ENV !== 'production' && exception instanceof Error
? {
errorName: exception.name,
errorMessage: exception.message,
stack: exception.stack,
}
: {};
console.error(JSON.stringify({
level:'error',
event:'request_failed',
status,
code,
correlationId:request.correlationId??null,
method:request.method,
path:request.path,
...errorDetails,
}));
}
response.status(status).json({
error:{
code,
params,
correlationId:request.correlationId??null,
},
});
}
private isHttpException(value:unknown):value is HttpException {
return value instanceof HttpException
|| (typeof value==='object' && value!==null
&& typeof (value as any).getStatus==='function'
&& typeof (value as any).getResponse==='function');
}
private safeParams(params:Record<string,unknown>) {
const blocked=new Set([
'password','secret','token','authorization','cookie','email','phone',
'card','pan','cvv','cvc','rawBody','body',
]);
const out:Record<string,unknown>={};
for(const [key,value] of Object.entries(params??{})) {
if(blocked.has(key) || /secret|token|password|authorization|cookie|pan|cvv|cvc/i.test(key)) continue;
if(typeof value==='string' && value.length>500) out[key]=`${value.slice(0,500)}…`;
else out[key]=value;
}
return out;
}
}
