codeflash-internal/js/cf-api/utils/error-handler.ts

418 lines
13 KiB
TypeScript
Raw Normal View History

import { Request, Response, NextFunction } from "express"
import * as Sentry from "@sentry/node"
import { logger, LogContext } from "../utils/logger.js"
// Simple error handling without categories
// Simple error classes
export class ApiError extends Error {
public readonly statusCode: number
public readonly isOperational: boolean
public readonly context?: Record<string, any>
constructor(
message: string,
statusCode: number = 500,
isOperational: boolean = true,
context?: Record<string, any>,
) {
super(message)
this.name = this.constructor.name
this.statusCode = statusCode
this.isOperational = isOperational
this.context = context
// Maintains proper stack trace for where our error was thrown
Error.captureStackTrace(this, this.constructor)
}
}
// Validation errors
export class ValidationError extends ApiError {
constructor(message: string, context?: Record<string, any>) {
super(message, 400, true, context)
}
}
// Authentication errors
export class AuthenticationError extends ApiError {
constructor(message: string = "Authentication failed", context?: Record<string, any>) {
super(message, 401, true, context)
}
}
// Authorization errors
export class AuthorizationError extends ApiError {
constructor(message: string = "Access denied", context?: Record<string, any>) {
super(message, 403, true, context)
}
}
// Not found errors
export class NotFoundError extends ApiError {
constructor(message: string = "Resource not found", context?: Record<string, any>) {
super(message, 404, true, context)
}
}
// Rate limit errors
export class RateLimitError extends ApiError {
constructor(message: string = "Rate limit exceeded", context?: Record<string, any>) {
super(message, 429, true, context)
}
}
// External service errors
export class ExternalServiceError extends ApiError {
constructor(service: string, message?: string, context?: Record<string, any>) {
super(message || `External service error: ${service}`, 502, true, { service, ...context })
}
}
// Database errors
export class DatabaseError extends ApiError {
constructor(message: string, context?: Record<string, any>) {
super(message, 500, false, context)
}
}
// Configuration errors
export class ConfigurationError extends ApiError {
constructor(message: string, context?: Record<string, any>) {
super(message, 500, false, context)
}
}
// Business logic errors
export class BusinessLogicError extends ApiError {
constructor(message: string, context?: Record<string, any>) {
super(message, 400, true, context)
}
}
// Error response interface
interface ErrorResponse {
error: {
message: string
code: string
statusCode: number
requestId?: string
timestamp: string
context?: Record<string, any>
}
}
// Global error handler class
export class GlobalErrorHandler {
/**
* Main error handling middleware
* Should be registered as the last middleware in the Express app
*/
static handle(error: Error, req: Request, res: Response, next: NextFunction): void {
// If response was already sent, delegate to default Express error handler
if (res.headersSent) {
return next(error)
}
// Create error context for logging
const errorContext: LogContext = {
requestId: req.requestId,
traceId: req.traceId,
userId: (req as any).userId,
endpoint: req.path,
operation: "error_handling",
}
// Handle different types of errors
// ==========================================
// ERROR TYPE CLASSIFICATION & SENTRY BEHAVIOR:
// ==========================================
//
// 1. ApiError (Custom Error Classes):
// - ValidationError, AuthenticationError, DatabaseError, etc.
// - Sentry: ✅ 5xx errors sent to Sentry, ❌ 4xx errors NOT sent
// - Production Logs: ERROR level with statusCode, duration, error details
// - Examples: DatabaseError(500) → Sentry, ValidationError(400) → No Sentry
//
// 2. Operational Errors (Expected Errors):
// - User input errors, business logic errors, expected failures
// - Sentry: ❌ NEVER sent to Sentry (expected behavior)
// - Production Logs: WARN level with basic error info
// - Examples: Invalid JSON, missing required fields, business rule violations
//
// 3. Programming Errors (Unexpected Errors):
// - Unhandled exceptions, bugs, infrastructure failures
// - Sentry: ✅ ALWAYS sent to Sentry (critical issues)
// - Production Logs: ERROR level with full stack trace and error details
// - Examples: Null pointer exceptions, unhandled promise rejections, system crashes
//
// PRODUCTION LOG SUMMARY:
// - ApiError 5xx: ERROR + Sentry (Critical infrastructure issues)
// - ApiError 4xx: ERROR + No Sentry (Expected client errors)
// - Operational: WARN + No Sentry (Expected business logic issues)
// - Programming: ERROR + Sentry (Unexpected system failures)
if (error instanceof ApiError) {
// Custom error classes (ValidationError, DatabaseError, etc.)
// Sentry: 5xx errors YES, 4xx errors NO
// Production: ERROR level with statusCode and error details
GlobalErrorHandler.handleApiError(error, req, res, errorContext)
} else if (GlobalErrorHandler.isOperationalError(error)) {
// Expected errors (user input, business logic)
// Sentry: NO (expected behavior)
// Production: WARN level with basic error info
GlobalErrorHandler.handleOperationalError(error, req, res, errorContext)
} else {
// Unexpected errors (bugs, infrastructure failures)
// Sentry: YES (critical issues)
// Production: ERROR level with full stack trace
GlobalErrorHandler.handleProgrammingError(error, req, res, errorContext)
}
}
/**
* Handle custom API errors (ValidationError, DatabaseError, etc.)
*
* SENTRY BEHAVIOR:
* - 5xx errors (500, 502, 503) Sent to Sentry (critical infrastructure)
* - 4xx errors (400, 401, 403, 404, 429) NOT sent to Sentry (expected client errors)
*
* PRODUCTION LOGS:
* - Level: ERROR
* - Content: timestamp, level, message, requestId, endpoint, userId, statusCode, duration, error details
* - Example: {"timestamp":"2024-01-01T10:00:00.000Z","level":"ERROR","message":"Database connection failed","requestId":"req-123","endpoint":"/cfapi/optimize","userId":"user-456","statusCode":500,"duration":2500,"error":{"name":"DatabaseError","message":"Connection timeout","stack":"..."}}
*/
private static handleApiError(
error: ApiError,
req: Request,
res: Response,
context: LogContext,
): void {
const errorResponse = GlobalErrorHandler.formatErrorResponse(error, req)
// Use errorWithSentry for critical errors, error for operational errors
if (error.statusCode >= 500 || !error.isOperational) {
logger.errorWithSentry(
`API Error: ${error.message}`,
context,
{
errorType: error.constructor.name,
statusCode: error.statusCode,
isOperational: error.isOperational,
context: error.context,
},
error,
)
} else {
logger.error(
`API Error: ${error.message}`,
context,
{
errorType: error.constructor.name,
statusCode: error.statusCode,
isOperational: error.isOperational,
context: error.context,
},
error,
)
}
// Send response
res.status(error.statusCode).json(errorResponse)
}
/**
* Handle operational errors (expected errors like user input validation)
*
* SENTRY BEHAVIOR:
* - NEVER sent to Sentry (these are expected user/business logic errors)
*
* PRODUCTION LOGS:
* - Level: WARN
* - Content: timestamp, level, message, requestId, endpoint, userId, errorType, statusCode
* - Example: {"timestamp":"2024-01-01T10:00:00.000Z","level":"WARN","message":"Operational Error: Invalid email format","requestId":"req-123","endpoint":"/cfapi/create-user","userId":"user-456","errorType":"ValidationError","statusCode":400}
*
* EXAMPLES:
* - Invalid JSON in request body
* - Missing required fields
* - Business rule violations
* - User permission issues
*/
private static handleOperationalError(
error: Error,
req: Request,
res: Response,
context: LogContext,
): void {
const apiError = new ApiError(error.message, 400, true)
const errorResponse = GlobalErrorHandler.formatErrorResponse(apiError, req)
// Log as warning for operational errors (no Sentry)
logger.warn(
`Operational Error: ${error.message}`,
context,
{
errorType: error.constructor.name,
statusCode: 400,
},
error,
)
res.status(400).json(errorResponse)
}
/**
* Handle programming errors (unexpected errors like bugs and infrastructure failures)
*
* SENTRY BEHAVIOR:
* - ALWAYS sent to Sentry (these are critical system issues that need immediate attention)
*
* PRODUCTION LOGS:
* - Level: ERROR
* - Content: timestamp, level, message, requestId, endpoint, userId, errorType, statusCode, isOperational, stack
* - Example: {"timestamp":"2024-01-01T10:00:00.000Z","level":"ERROR","message":"Programming Error: Cannot read property 'id' of undefined","requestId":"req-123","endpoint":"/cfapi/optimize","userId":"user-456","errorType":"TypeError","statusCode":500,"isOperational":false,"stack":"TypeError: Cannot read property 'id' of undefined\n at UserService.getUser (/app/services/user.js:45:12)\n at ..."}
*
* EXAMPLES:
* - Null pointer exceptions
* - Unhandled promise rejections
* - System crashes
* - Infrastructure failures
* - Memory leaks
* - Uncaught exceptions
*/
private static handleProgrammingError(
error: Error,
req: Request,
res: Response,
context: LogContext,
): void {
const apiError = new ApiError(
process.env.NODE_ENV === "production" ? "Internal server error" : error.message,
500,
false,
)
const errorResponse = GlobalErrorHandler.formatErrorResponse(apiError, req)
// Log as error for programming errors (always send to Sentry)
logger.errorWithSentry(
`Programming Error: ${error.message}`,
context,
{
errorType: error.constructor.name,
statusCode: 500,
isOperational: false,
stack: error.stack,
},
error,
)
res.status(500).json(errorResponse)
}
/**
* Format error response for API consumers
*/
private static formatErrorResponse(error: ApiError, req: Request): ErrorResponse {
return {
error: {
message: error.message,
code: error.constructor.name,
statusCode: error.statusCode,
requestId: req.requestId,
timestamp: new Date().toISOString(),
...(error.context && { context: error.context }),
},
}
}
/**
* Determine if an error is operational (expected) or programming (unexpected)
*/
private static isOperationalError(error: Error): boolean {
if (error instanceof ApiError) {
return error.isOperational
}
// Common operational errors
const operationalErrors = [
"ValidationError",
"CastError",
"JsonWebTokenError",
"TokenExpiredError",
"MulterError",
]
return operationalErrors.includes(error.name)
}
/**
* Handle unhandled promise rejections
*
* SENTRY BEHAVIOR:
* - ALWAYS sent to Sentry for monitoring
*
* SYSTEM BEHAVIOR:
* - Does NOT exit the process
* - Unhandled rejections don't corrupt process state
* - Service continues running after logging
*/
static async handleUnhandledRejection(reason: any, promise: Promise<any>): Promise<void> {
const context: LogContext = {
operation: "unhandled_rejection",
}
const error = reason instanceof Error ? reason : new Error(String(reason))
logger.errorWithSentry(
"Unhandled Promise Rejection",
context,
{
reason: reason?.toString(),
promise: promise.toString(),
isOperational: false,
},
error,
)
// Do NOT exit - unhandled rejections don't corrupt process state
}
/**
* Handle uncaught exceptions
*
* SENTRY BEHAVIOR:
* - ALWAYS sent to Sentry for monitoring
*
* SYSTEM BEHAVIOR:
* - Does NOT exit the process
* - Modern Node.js error handling makes process exit less necessary
* - Service continues running after logging
*/
static async handleUncaughtException(error: Error): Promise<void> {
const context: LogContext = {
operation: "uncaught_exception",
}
logger.errorWithSentry(
"Uncaught Exception",
context,
{
stack: error.stack,
isOperational: false,
},
error,
)
// Do NOT exit - keep service running
}
}
// Convenience function for creating API errors
export function createApiError(
message: string,
statusCode: number = 500,
context?: Record<string, any>,
): ApiError {
return new ApiError(message, statusCode, true, context)
}