not sure why those errors got removed we'll see
This commit is contained in:
@@ -1,94 +1,53 @@
|
||||
// src/middleware/errorHandler.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import {
|
||||
DatabaseError,
|
||||
UniqueConstraintError,
|
||||
ForeignKeyConstraintError,
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
ValidationIssue,
|
||||
} from '../services/db/errors.db';
|
||||
import crypto from 'crypto';
|
||||
import { ZodError } from 'zod';
|
||||
import { NotFoundError, UniqueConstraintError, ValidationError } from '../services/db/errors.db';
|
||||
import { logger } from '../services/logger.server';
|
||||
|
||||
interface HttpError extends Error {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export const errorHandler = (err: HttpError, req: Request, res: Response, next: NextFunction) => {
|
||||
// If the response headers have already been sent, we must delegate to the default Express error handler.
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
// The pino-http middleware guarantees that `req.log` will be available.
|
||||
const log = req.log;
|
||||
// Use the request-scoped logger if available, otherwise fall back to the global logger.
|
||||
const log = req.log || logger;
|
||||
|
||||
// --- 1. Determine Final Status Code and Message ---
|
||||
let statusCode = err.status ?? 500;
|
||||
const message = err.message;
|
||||
let validationIssues: ValidationIssue[] | undefined;
|
||||
let errorId: string | undefined;
|
||||
|
||||
// Refine the status code for known error types. Check for most specific types first.
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
statusCode = 409; // Conflict
|
||||
} else if (err instanceof NotFoundError) {
|
||||
statusCode = 404;
|
||||
} else if (err instanceof ForeignKeyConstraintError) {
|
||||
statusCode = 400;
|
||||
} else if (err instanceof ValidationError) {
|
||||
statusCode = 400;
|
||||
validationIssues = err.validationErrors;
|
||||
} else if (err instanceof DatabaseError) {
|
||||
// This is a generic fallback for other database errors that are not the specific subclasses above.
|
||||
statusCode = err.status;
|
||||
} else if (err.name === 'UnauthorizedError') {
|
||||
statusCode = err.status || 401;
|
||||
// --- Handle Zod Validation Errors ---
|
||||
if (err instanceof ZodError) {
|
||||
log.warn({ err: err.flatten() }, 'Request validation failed');
|
||||
return res.status(400).json({
|
||||
message: 'The request data is invalid.',
|
||||
errors: err.errors.map((e) => ({ path: e.path, message: e.message })),
|
||||
});
|
||||
}
|
||||
|
||||
// --- 2. Log Based on Final Status Code ---
|
||||
// Log the full error details for debugging, especially for server errors.
|
||||
if (statusCode >= 500) {
|
||||
errorId = crypto.randomBytes(4).toString('hex');
|
||||
// The request-scoped logger already contains user, IP, and request_id.
|
||||
// We add the full error and the request object itself.
|
||||
// Pino's `redact` config will automatically sanitize sensitive fields in `req`.
|
||||
log.error(
|
||||
{
|
||||
err,
|
||||
errorId,
|
||||
req: { method: req.method, url: req.originalUrl, headers: req.headers, body: req.body },
|
||||
},
|
||||
`Unhandled API Error (ID: ${errorId})`,
|
||||
);
|
||||
} else {
|
||||
// For 4xx errors, log at a lower level (e.g., 'warn') to avoid flooding error trackers.
|
||||
// We include the validation errors in the log context if they exist.
|
||||
log.warn(
|
||||
{
|
||||
err,
|
||||
validationErrors: validationIssues, // Add validation issues to the log object
|
||||
statusCode,
|
||||
},
|
||||
`Client Error on ${req.method} ${req.path}: ${message}`,
|
||||
);
|
||||
// --- Handle Custom Operational Errors ---
|
||||
if (err instanceof NotFoundError) {
|
||||
log.info({ err }, 'Resource not found');
|
||||
return res.status(404).json({ message: err.message });
|
||||
}
|
||||
|
||||
// --- TEST ENVIRONMENT DEBUGGING ---
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
console.error('--- [TEST] UNHANDLED ERROR ---', err);
|
||||
if (err instanceof UniqueConstraintError || err instanceof ValidationError) {
|
||||
log.warn({ err }, 'Constraint or validation error occurred');
|
||||
return res.status(400).json({ message: err.message });
|
||||
}
|
||||
|
||||
// --- 3. Send Response ---
|
||||
// In production, send a generic message for 5xx errors.
|
||||
// In dev/test, send the actual error message for easier debugging.
|
||||
const responseMessage =
|
||||
statusCode >= 500 && process.env.NODE_ENV === 'production'
|
||||
? `An unexpected server error occurred. Please reference error ID: ${errorId}`
|
||||
: message;
|
||||
// --- Handle Generic Errors ---
|
||||
// Log the full error object for debugging. The pino logger will handle redaction.
|
||||
log.error({ err }, 'An unhandled error occurred in an Express route');
|
||||
|
||||
res.status(statusCode).json({
|
||||
message: responseMessage,
|
||||
...(validationIssues && { errors: validationIssues }), // Conditionally add the 'errors' array if it exists
|
||||
});
|
||||
};
|
||||
// In production, send a generic message to avoid leaking implementation details.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return res.status(500).json({ message: 'An internal server error occurred.' });
|
||||
}
|
||||
|
||||
// In development, send more details for easier debugging.
|
||||
return res.status(500).json({ message: err.message, stack: err.stack });
|
||||
};
|
||||
Reference in New Issue
Block a user