101 lines
3.9 KiB
TypeScript
101 lines
3.9 KiB
TypeScript
// src/middleware/errorHandler.ts
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import crypto from 'crypto';
|
|
import { ZodError } from 'zod';
|
|
import {
|
|
ForeignKeyConstraintError,
|
|
NotFoundError,
|
|
UniqueConstraintError,
|
|
ValidationError,
|
|
} from '../services/db/errors.db';
|
|
import { logger } from '../services/logger.server';
|
|
|
|
/**
|
|
* A centralized error handling middleware for the Express application.
|
|
* This middleware should be the LAST `app.use()` call to catch all errors from previous routes and middleware.
|
|
*
|
|
* It standardizes error responses and ensures consistent logging.
|
|
*/
|
|
export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
// If headers have already been sent, delegate to the default Express error handler.
|
|
if (res.headersSent) {
|
|
return next(err);
|
|
}
|
|
|
|
// Use the request-scoped logger if available, otherwise fall back to the global logger.
|
|
const log = req.log || logger;
|
|
|
|
// --- Handle Zod Validation Errors (from validateRequest middleware) ---
|
|
if (err instanceof ZodError) {
|
|
const statusCode = 400;
|
|
const message = 'The request data is invalid.';
|
|
const errors = err.issues.map((e) => ({ path: e.path, message: e.message }));
|
|
log.warn({ err, validationErrors: errors, statusCode }, `Client Error on ${req.method} ${req.path}: ${message}`);
|
|
return res.status(statusCode).json({ message, errors });
|
|
}
|
|
|
|
// --- Handle Custom Operational Errors ---
|
|
if (err instanceof NotFoundError) {
|
|
const statusCode = 404;
|
|
log.warn({ err, statusCode }, `Client Error on ${req.method} ${req.path}: ${err.message}`);
|
|
return res.status(statusCode).json({ message: err.message });
|
|
}
|
|
|
|
if (err instanceof ValidationError) {
|
|
const statusCode = 400;
|
|
log.warn(
|
|
{ err, validationErrors: err.validationErrors, statusCode },
|
|
`Client Error on ${req.method} ${req.path}: ${err.message}`,
|
|
);
|
|
return res.status(statusCode).json({ message: err.message, errors: err.validationErrors });
|
|
}
|
|
|
|
if (err instanceof UniqueConstraintError) {
|
|
const statusCode = 409;
|
|
log.warn({ err, statusCode }, `Client Error on ${req.method} ${req.path}: ${err.message}`);
|
|
return res.status(statusCode).json({ message: err.message }); // Use 409 Conflict for unique constraints
|
|
}
|
|
|
|
if (err instanceof ForeignKeyConstraintError) {
|
|
const statusCode = 400;
|
|
log.warn({ err, statusCode }, `Client Error on ${req.method} ${req.path}: ${err.message}`);
|
|
return res.status(statusCode).json({ message: err.message });
|
|
}
|
|
|
|
// --- Handle Generic Client Errors (e.g., from express-jwt, or manual status setting) ---
|
|
let status = (err as any).status || (err as any).statusCode;
|
|
// Default UnauthorizedError to 401 if no status is present, a common case for express-jwt.
|
|
if (err.name === 'UnauthorizedError' && !status) {
|
|
status = 401;
|
|
}
|
|
if (status && status >= 400 && status < 500) {
|
|
log.warn({ err, statusCode: status }, `Client Error on ${req.method} ${req.path}: ${err.message}`);
|
|
return res.status(status).json({ message: err.message });
|
|
}
|
|
|
|
// --- Handle All Other (500-level) Errors ---
|
|
const errorId = crypto.randomBytes(4).toString('hex');
|
|
log.error(
|
|
{
|
|
err,
|
|
errorId,
|
|
req: { method: req.method, url: req.url, headers: req.headers, body: req.body },
|
|
},
|
|
`Unhandled API Error (ID: ${errorId})`,
|
|
);
|
|
|
|
// Also log to console in test environment for visibility in test runners
|
|
if (process.env.NODE_ENV === 'test') {
|
|
console.error(`--- [TEST] UNHANDLED ERROR (ID: ${errorId}) ---`, err);
|
|
}
|
|
|
|
// In production, send a generic message to avoid leaking implementation details.
|
|
if (process.env.NODE_ENV === 'production') {
|
|
return res.status(500).json({
|
|
message: `An unexpected server error occurred. Please reference error ID: ${errorId}`,
|
|
});
|
|
}
|
|
|
|
// In non-production environments (dev, test, etc.), send more details for easier debugging.
|
|
return res.status(500).json({ message: err.message, stack: err.stack, errorId });
|
|
}; |