initial commit
This commit is contained in:
102
apps/api/src/common/filters/rfc7807-exception.filter.ts
Normal file
102
apps/api/src/common/filters/rfc7807-exception.filter.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
ArgumentsHost,
|
||||
Catch,
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
import { createProblemDetails, type ProblemDetails } from '@hexahost/contracts';
|
||||
|
||||
@Catch()
|
||||
export class Rfc7807ExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: unknown, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<FastifyReply>();
|
||||
const request = ctx.getRequest<FastifyRequest>();
|
||||
|
||||
const status = this.resolveStatus(exception);
|
||||
const problem = this.toProblemDetails(exception, status, request.url);
|
||||
|
||||
void response
|
||||
.status(status)
|
||||
.header('Content-Type', 'application/problem+json')
|
||||
.send(problem);
|
||||
}
|
||||
|
||||
private resolveStatus(exception: unknown): number {
|
||||
if (exception instanceof HttpException) {
|
||||
return exception.getStatus();
|
||||
}
|
||||
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
|
||||
private toProblemDetails(
|
||||
exception: unknown,
|
||||
status: number,
|
||||
instance: string,
|
||||
): ProblemDetails {
|
||||
if (exception instanceof HttpException) {
|
||||
const response = exception.getResponse();
|
||||
|
||||
if (typeof response === 'object' && response !== null) {
|
||||
const body = response as Record<string, unknown>;
|
||||
|
||||
if (typeof body['status'] === 'string' && Array.isArray(body['checks'])) {
|
||||
return createProblemDetails({
|
||||
title: 'Service Unavailable',
|
||||
status,
|
||||
detail: 'One or more readiness checks failed',
|
||||
instance,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof body['message'] === 'string') {
|
||||
return createProblemDetails({
|
||||
title: exception.name,
|
||||
status,
|
||||
detail: body['message'],
|
||||
instance,
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(body['message'])) {
|
||||
return createProblemDetails({
|
||||
title: 'Validation Error',
|
||||
status,
|
||||
detail: 'Request validation failed',
|
||||
instance,
|
||||
errors: body['message'].map((message) => ({
|
||||
message: String(message),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return createProblemDetails({
|
||||
title: exception.name,
|
||||
status,
|
||||
detail: exception.message,
|
||||
instance,
|
||||
});
|
||||
}
|
||||
|
||||
if (exception instanceof Error) {
|
||||
return createProblemDetails({
|
||||
title: 'Internal Server Error',
|
||||
status,
|
||||
detail: exception.message,
|
||||
instance,
|
||||
});
|
||||
}
|
||||
|
||||
return createProblemDetails({
|
||||
title: 'Internal Server Error',
|
||||
status,
|
||||
detail: 'An unexpected error occurred',
|
||||
instance,
|
||||
});
|
||||
}
|
||||
}
|
||||
20
apps/api/src/common/middleware/request-id.middleware.ts
Normal file
20
apps/api/src/common/middleware/request-id.middleware.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
export class RequestIdMiddleware implements NestMiddleware {
|
||||
use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void): void {
|
||||
const incoming =
|
||||
(req.headers['x-request-id'] as string | undefined) ??
|
||||
(req.headers['x-correlation-id'] as string | undefined);
|
||||
|
||||
const requestId = incoming && incoming.trim().length > 0 ? incoming : randomUUID();
|
||||
|
||||
req.headers['x-request-id'] = requestId;
|
||||
res.setHeader('x-request-id', requestId);
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user