2026-10-03 10:36:03 +00:00
|
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
|
|
|
|
|
|
import type { HttpErrorResponseDTO } from "@metis/core/common";
|
|
|
|
|
import { registerCoreRoutes } from "@metis/core/module";
|
|
|
|
|
import type { Application, ErrorRequestHandler, RequestHandler } from "express";
|
|
|
|
|
import express from "express";
|
|
|
|
|
import helmet from "helmet";
|
|
|
|
|
|
2026-10-15 16:39:37 +00:00
|
|
|
import { httpErrorDiagnostics } from "./infrastructure/http/http-error-diagnostic";
|
2026-10-03 10:36:03 +00:00
|
|
|
import type { HttpLogger } from "./infrastructure/http/http-logger";
|
|
|
|
|
import { logHttpRequest } from "./infrastructure/http/http-logger";
|
|
|
|
|
|
|
|
|
|
export function createApp(
|
|
|
|
|
logger: HttpLogger = logHttpRequest,
|
|
|
|
|
identity?: { middleware: RequestHandler; register: (app: Application) => void }
|
|
|
|
|
) {
|
|
|
|
|
const app = express();
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
app.disable("x-powered-by");
|
|
|
|
|
app.use(helmet());
|
|
|
|
|
app.use((request, response, next) => {
|
|
|
|
|
const requestId = randomUUID();
|
|
|
|
|
const startedAt = performance.now();
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
response.setHeader("x-request-id", requestId);
|
|
|
|
|
response.setHeader("cache-control", "no-store");
|
|
|
|
|
response.once("finish", () => {
|
|
|
|
|
logger({
|
|
|
|
|
event: "http_request",
|
|
|
|
|
requestId,
|
|
|
|
|
method: request.method,
|
|
|
|
|
statusCode: response.statusCode,
|
|
|
|
|
durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.use(express.json({ limit: "64kb" }));
|
|
|
|
|
if (identity) {
|
|
|
|
|
app.use(identity.middleware);
|
|
|
|
|
identity.register(app);
|
|
|
|
|
}
|
|
|
|
|
registerCoreRoutes(app);
|
|
|
|
|
|
|
|
|
|
app.use((_request, response) => {
|
|
|
|
|
const error: HttpErrorResponseDTO = {
|
|
|
|
|
code: "NOT_FOUND",
|
|
|
|
|
requestId: response.getHeader("x-request-id")?.toString() ?? "",
|
|
|
|
|
};
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
response.status(404).json(error);
|
|
|
|
|
});
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-15 16:39:37 +00:00
|
|
|
const errorHandler: ErrorRequestHandler = (failure: unknown, request, response, next) => {
|
2026-10-03 10:36:03 +00:00
|
|
|
if (response.headersSent) {
|
2026-10-08 12:14:01 +00:00
|
|
|
next(failure);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-10-08 12:14:01 +00:00
|
|
|
const status = bodyFailureStatus(failure);
|
2026-10-15 16:39:37 +00:00
|
|
|
|
|
|
|
|
if (status === 500) {
|
|
|
|
|
logger({
|
|
|
|
|
event: "http_error",
|
|
|
|
|
requestId: response.getHeader("x-request-id")?.toString() ?? "",
|
|
|
|
|
method: request.method,
|
|
|
|
|
statusCode: 500,
|
|
|
|
|
errors: httpErrorDiagnostics(failure),
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-10-03 10:36:03 +00:00
|
|
|
const error: HttpErrorResponseDTO = {
|
2026-10-08 12:14:01 +00:00
|
|
|
code: status === 500 ? "INTERNAL_ERROR" : "INVALID_REQUEST",
|
2026-10-03 10:36:03 +00:00
|
|
|
requestId: response.getHeader("x-request-id")?.toString() ?? "",
|
|
|
|
|
};
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-08 12:14:01 +00:00
|
|
|
response.status(status).json(error);
|
2026-10-03 10:36:03 +00:00
|
|
|
};
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
app.use(errorHandler);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
return app;
|
|
|
|
|
}
|
2026-10-08 12:14:01 +00:00
|
|
|
|
|
|
|
|
function bodyFailureStatus(failure: unknown): number {
|
|
|
|
|
if (!(failure instanceof Error && "type" in failure)) {
|
|
|
|
|
return 500;
|
|
|
|
|
}
|
|
|
|
|
// Solo se reconocen errores públicos del parser, nunca mensajes o status arbitrarios.
|
|
|
|
|
switch (failure.type) {
|
|
|
|
|
case "entity.parse.failed":
|
|
|
|
|
case "request.aborted":
|
|
|
|
|
case "request.size.invalid":
|
|
|
|
|
return 400;
|
|
|
|
|
case "entity.too.large":
|
|
|
|
|
return 413;
|
|
|
|
|
case "charset.unsupported":
|
|
|
|
|
case "encoding.unsupported":
|
|
|
|
|
return 415;
|
|
|
|
|
default:
|
|
|
|
|
return 500;
|
|
|
|
|
}
|
|
|
|
|
}
|