metis-desk/apps/server/src/app.ts

104 lines
2.9 KiB
TypeScript
Raw Normal View History

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";
import { httpErrorDiagnostics } from "./infrastructure/http/http-error-diagnostic";
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
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
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
response.status(404).json(error);
});
2026-10-10 10:01:58 +00:00
const errorHandler: ErrorRequestHandler = (failure: unknown, request, response, next) => {
if (response.headersSent) {
2026-10-08 12:14:01 +00:00
next(failure);
2026-10-10 10:01:58 +00:00
return;
}
2026-10-08 12:14:01 +00:00
const status = bodyFailureStatus(failure);
if (status === 500) {
logger({
event: "http_error",
requestId: response.getHeader("x-request-id")?.toString() ?? "",
method: request.method,
statusCode: 500,
errors: httpErrorDiagnostics(failure),
});
}
const error: HttpErrorResponseDTO = {
2026-10-08 12:14:01 +00:00
code: status === 500 ? "INTERNAL_ERROR" : "INVALID_REQUEST",
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-10 10:01:58 +00:00
app.use(errorHandler);
2026-10-10 10:01:58 +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;
}
}