Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 17x 17x 17x 17x 420x 17x 17x 4x 4x 4x 4x 17x 420x 324x 17x 17x 420x 145x 2x 17x 420x 17x 17x 49x 421x 1x 17x 420x 7x 7x 421x 7x 7x 19x 4x 4x 4x 4x 4x 4x 4x 4x 418x 68x 68x 68x 350x 178x 178x 17x 17x 178x 172x 6x 17x 17x 6x 6x 171x 6x 6x 6x 6x 167x 167x 6x 421x 6x 167x 22x 6x 6x 6x 6x 6x 17x 22x 2x 2x 5x 5x 13x 13x 2x 2x 17x 22x 145x 145x 2x 145x 144x 2x 2x 2x 2x 2x 2x 145x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 144x 2x 2x 2x 2x 2x 2x 2x 2x | // src/middleware/errorHandler.ts // ============================================================================ // CENTRALIZED ERROR HANDLING MIDDLEWARE // ============================================================================ // This middleware standardizes all error responses per ADR-028. // It should be the LAST `app.use()` call to catch all errors. // ============================================================================ 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'; import { ErrorCode, ApiErrorResponse } from '../types/api'; /** * Helper to send standardized error responses. */ function sendErrorResponse( res: Response, statusCode: number, code: string, message: string, details?: unknown, meta?: { requestId?: string; timestamp?: string }, ): Response<ApiErrorResponse> { const response: ApiErrorResponse = { success: false, error: { code, message, }, }; if (details !== undefined) { response.error.details = details; } if (meta) { response.meta = meta; } return res.status(statusCode).json(response); } /** * 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 per ADR-028 and ensures consistent logging per ADR-004. */ 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 details = err.issues.map((e) => ({ path: e.path, message: e.message })); log.warn( { err, validationErrors: details, statusCode }, `Client Error on ${req.method} ${req.path}: ${message}`, ); return sendErrorResponse(res, statusCode, ErrorCode.VALIDATION_ERROR, message, details); } // --- 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 sendErrorResponse(res, statusCode, ErrorCode.NOT_FOUND, 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 sendErrorResponse( res, statusCode, ErrorCode.VALIDATION_ERROR, err.message, err.validationErrors, ); } if (err instanceof UniqueConstraintError) { const statusCode = 409; log.warn({ err, statusCode }, `Client Error on ${req.method} ${req.path}: ${err.message}`); return sendErrorResponse(res, statusCode, ErrorCode.CONFLICT, err.message); } if (err instanceof ForeignKeyConstraintError) { const statusCode = 400; log.warn({ err, statusCode }, `Client Error on ${req.method} ${req.path}: ${err.message}`); return sendErrorResponse(res, statusCode, ErrorCode.BAD_REQUEST, err.message); } // --- Handle Generic Client Errors (e.g., from express-jwt, or manual status setting) --- const errWithStatus = err as Error & { status?: number; statusCode?: number }; let status = errWithStatus.status || errWithStatus.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}`, ); // Map status codes to error codes let errorCode: string; switch (status) { case 400: errorCode = ErrorCode.BAD_REQUEST; break; case 401: errorCode = ErrorCode.UNAUTHORIZED; break; case 403: errorCode = ErrorCode.FORBIDDEN; break; case 404: errorCode = ErrorCode.NOT_FOUND; break; case 409: errorCode = ErrorCode.CONFLICT; break; case 429: errorCode = ErrorCode.RATE_LIMITED; break; default: errorCode = ErrorCode.BAD_REQUEST; } return sendErrorResponse(res, status, errorCode, 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/staging environments for visibility in test runners if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'staging') { console.error( `--- [${process.env.NODE_ENV?.toUpperCase()}] UNHANDLED ERROR (ID: ${errorId}) ---`, err, ); } // In production, send a generic message to avoid leaking implementation details. if (process.env.NODE_ENV === 'production') { return sendErrorResponse( res, 500, ErrorCode.INTERNAL_ERROR, `An unexpected server error occurred. Please reference error ID: ${errorId}`, undefined, { requestId: errorId }, ); } // In non-production environments (dev, test, etc.), send more details for easier debugging. return sendErrorResponse( res, 500, ErrorCode.INTERNAL_ERROR, err.message, { stack: err.stack }, { requestId: errorId }, ); }; |