412 lines
12 KiB
TypeScript
412 lines
12 KiB
TypeScript
|
import { PlatformError } from './base.errors.js';
|
||
|
import type { IErrorContext } from './base.errors.js';
|
||
|
import { ErrorCategory, ErrorRecoverability, ErrorSeverity } from './error.codes.js';
|
||
|
import { logger } from '../logger.js';
|
||
|
|
||
|
/**
|
||
|
* Error handler configuration
|
||
|
*/
|
||
|
export interface IErrorHandlerConfig {
|
||
|
/** Whether to log errors automatically */
|
||
|
logErrors: boolean;
|
||
|
|
||
|
/** Whether to include stack traces in prod environment */
|
||
|
includeStacksInProd: boolean;
|
||
|
|
||
|
/** Default retry options */
|
||
|
retry: {
|
||
|
/** Maximum retry attempts */
|
||
|
maxAttempts: number;
|
||
|
|
||
|
/** Base delay between retries in ms */
|
||
|
baseDelay: number;
|
||
|
|
||
|
/** Maximum delay between retries in ms */
|
||
|
maxDelay: number;
|
||
|
|
||
|
/** Backoff factor for exponential backoff */
|
||
|
backoffFactor: number;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Global error handler configuration
|
||
|
*/
|
||
|
const config: IErrorHandlerConfig = {
|
||
|
logErrors: true,
|
||
|
includeStacksInProd: false,
|
||
|
retry: {
|
||
|
maxAttempts: 3,
|
||
|
baseDelay: 1000,
|
||
|
maxDelay: 30000,
|
||
|
backoffFactor: 2
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Error handler utility
|
||
|
* Provides methods for consistent error handling across the platform
|
||
|
*/
|
||
|
export class ErrorHandler {
|
||
|
/**
|
||
|
* Current configuration
|
||
|
*/
|
||
|
public static config = config;
|
||
|
|
||
|
/**
|
||
|
* Update error handler configuration
|
||
|
*
|
||
|
* @param newConfig New configuration (partial)
|
||
|
*/
|
||
|
public static configure(newConfig: Partial<IErrorHandlerConfig>): void {
|
||
|
ErrorHandler.config = {
|
||
|
...ErrorHandler.config,
|
||
|
...newConfig,
|
||
|
retry: {
|
||
|
...ErrorHandler.config.retry,
|
||
|
...(newConfig.retry || {})
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Convert any error to a PlatformError
|
||
|
*
|
||
|
* @param error Error to convert
|
||
|
* @param defaultCode Default error code if not a PlatformError
|
||
|
* @param context Additional context
|
||
|
* @returns PlatformError instance
|
||
|
*/
|
||
|
public static toPlatformError(
|
||
|
error: any,
|
||
|
defaultCode: string,
|
||
|
context: IErrorContext = {}
|
||
|
): PlatformError {
|
||
|
// If already a PlatformError, just add context
|
||
|
if (error instanceof PlatformError) {
|
||
|
// Add context if provided
|
||
|
if (Object.keys(context).length > 0) {
|
||
|
return new (error.constructor as typeof PlatformError)(
|
||
|
error.message,
|
||
|
error.code,
|
||
|
error.severity,
|
||
|
error.category,
|
||
|
error.recoverability,
|
||
|
{
|
||
|
...error.context,
|
||
|
...context,
|
||
|
data: {
|
||
|
...(error.context.data || {}),
|
||
|
...(context.data || {})
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
return error;
|
||
|
}
|
||
|
|
||
|
// Convert standard Error to PlatformError
|
||
|
if (error instanceof Error) {
|
||
|
return new PlatformError(
|
||
|
error.message,
|
||
|
defaultCode,
|
||
|
ErrorSeverity.MEDIUM,
|
||
|
ErrorCategory.OPERATION,
|
||
|
ErrorRecoverability.NON_RECOVERABLE,
|
||
|
{
|
||
|
...context,
|
||
|
data: {
|
||
|
...(context.data || {}),
|
||
|
originalError: {
|
||
|
name: error.name,
|
||
|
message: error.message,
|
||
|
stack: error.stack
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Not an Error instance
|
||
|
return new PlatformError(
|
||
|
typeof error === 'string' ? error : 'Unknown error',
|
||
|
defaultCode,
|
||
|
ErrorSeverity.MEDIUM,
|
||
|
ErrorCategory.OPERATION,
|
||
|
ErrorRecoverability.NON_RECOVERABLE,
|
||
|
context
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Format an error for API responses
|
||
|
* Sanitizes errors for safe external exposure
|
||
|
*
|
||
|
* @param error Error to format
|
||
|
* @param includeDetails Whether to include detailed information
|
||
|
* @returns Formatted error object
|
||
|
*/
|
||
|
public static formatErrorForResponse(
|
||
|
error: any,
|
||
|
includeDetails: boolean = false
|
||
|
): Record<string, any> {
|
||
|
const platformError = ErrorHandler.toPlatformError(
|
||
|
error,
|
||
|
'PLATFORM_OPERATION_ERROR'
|
||
|
);
|
||
|
|
||
|
// Basic error information
|
||
|
const responseError: Record<string, any> = {
|
||
|
code: platformError.code,
|
||
|
message: platformError.getUserMessage(),
|
||
|
requestId: platformError.context.requestId
|
||
|
};
|
||
|
|
||
|
// Include more details if requested
|
||
|
if (includeDetails) {
|
||
|
responseError.details = {
|
||
|
severity: platformError.severity,
|
||
|
category: platformError.category,
|
||
|
rawMessage: platformError.message,
|
||
|
data: platformError.context.data
|
||
|
};
|
||
|
|
||
|
// Only include stack trace in non-production or if explicitly enabled
|
||
|
if (process.env.NODE_ENV !== 'production' || ErrorHandler.config.includeStacksInProd) {
|
||
|
responseError.details.stack = platformError.stack;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return responseError;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle an error with consistent logging and formatting
|
||
|
*
|
||
|
* @param error Error to handle
|
||
|
* @param defaultCode Default error code if not a PlatformError
|
||
|
* @param context Additional context
|
||
|
* @returns Formatted error for response
|
||
|
*/
|
||
|
public static handleError(
|
||
|
error: any,
|
||
|
defaultCode: string,
|
||
|
context: IErrorContext = {}
|
||
|
): Record<string, any> {
|
||
|
const platformError = ErrorHandler.toPlatformError(
|
||
|
error,
|
||
|
defaultCode,
|
||
|
context
|
||
|
);
|
||
|
|
||
|
// Log the error if enabled
|
||
|
if (ErrorHandler.config.logErrors) {
|
||
|
logger.error(platformError.message, {
|
||
|
error_code: platformError.code,
|
||
|
error_name: platformError.name,
|
||
|
error_severity: platformError.severity,
|
||
|
error_category: platformError.category,
|
||
|
error_recoverability: platformError.recoverability,
|
||
|
...platformError.context,
|
||
|
stack: platformError.stack
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Return formatted error for response
|
||
|
const isDetailedMode = process.env.NODE_ENV !== 'production';
|
||
|
return ErrorHandler.formatErrorForResponse(platformError, isDetailedMode);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Execute a function with error handling
|
||
|
*
|
||
|
* @param fn Function to execute
|
||
|
* @param defaultCode Default error code if the function throws
|
||
|
* @param context Additional context
|
||
|
* @returns Function result or error
|
||
|
*/
|
||
|
public static async execute<T>(
|
||
|
fn: () => Promise<T>,
|
||
|
defaultCode: string,
|
||
|
context: IErrorContext = {}
|
||
|
): Promise<T> {
|
||
|
try {
|
||
|
return await fn();
|
||
|
} catch (error) {
|
||
|
throw ErrorHandler.toPlatformError(error, defaultCode, context);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Execute a function with retries and exponential backoff
|
||
|
*
|
||
|
* @param fn Function to execute
|
||
|
* @param defaultCode Default error code if the function throws
|
||
|
* @param options Retry options
|
||
|
* @param context Additional context
|
||
|
* @returns Function result or error after max retries
|
||
|
*/
|
||
|
public static async executeWithRetry<T>(
|
||
|
fn: () => Promise<T>,
|
||
|
defaultCode: string,
|
||
|
options: {
|
||
|
maxAttempts?: number;
|
||
|
baseDelay?: number;
|
||
|
maxDelay?: number;
|
||
|
backoffFactor?: number;
|
||
|
retryableErrorCodes?: string[];
|
||
|
retryableErrorPatterns?: RegExp[];
|
||
|
onRetry?: (error: PlatformError, attempt: number, delay: number) => void;
|
||
|
} = {},
|
||
|
context: IErrorContext = {}
|
||
|
): Promise<T> {
|
||
|
const {
|
||
|
maxAttempts = ErrorHandler.config.retry.maxAttempts,
|
||
|
baseDelay = ErrorHandler.config.retry.baseDelay,
|
||
|
maxDelay = ErrorHandler.config.retry.maxDelay,
|
||
|
backoffFactor = ErrorHandler.config.retry.backoffFactor,
|
||
|
retryableErrorCodes = [],
|
||
|
retryableErrorPatterns = [],
|
||
|
onRetry = () => {}
|
||
|
} = options;
|
||
|
|
||
|
let lastError: PlatformError;
|
||
|
|
||
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||
|
try {
|
||
|
return await fn();
|
||
|
} catch (error) {
|
||
|
// Convert to PlatformError
|
||
|
const platformError = ErrorHandler.toPlatformError(
|
||
|
error,
|
||
|
defaultCode,
|
||
|
{
|
||
|
...context,
|
||
|
retry: {
|
||
|
currentRetry: attempt,
|
||
|
maxRetries: maxAttempts,
|
||
|
nextRetryAt: 0 // Will be set below if retrying
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
|
||
|
lastError = platformError;
|
||
|
|
||
|
// Check if we should retry
|
||
|
const isLastAttempt = attempt >= maxAttempts - 1;
|
||
|
|
||
|
if (isLastAttempt) {
|
||
|
// No more retries
|
||
|
throw platformError;
|
||
|
}
|
||
|
|
||
|
// Check if error is retryable
|
||
|
const isRetryable =
|
||
|
// Built-in recoverability
|
||
|
platformError.recoverability === ErrorRecoverability.RECOVERABLE ||
|
||
|
platformError.recoverability === ErrorRecoverability.MAYBE_RECOVERABLE ||
|
||
|
platformError.recoverability === ErrorRecoverability.TRANSIENT ||
|
||
|
// Specifically included error codes
|
||
|
retryableErrorCodes.includes(platformError.code) ||
|
||
|
// Matches error message patterns
|
||
|
retryableErrorPatterns.some(pattern => pattern.test(platformError.message));
|
||
|
|
||
|
if (!isRetryable) {
|
||
|
throw platformError;
|
||
|
}
|
||
|
|
||
|
// Calculate delay with exponential backoff
|
||
|
const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
|
||
|
|
||
|
// Add jitter to prevent thundering herd problem (±20%)
|
||
|
const jitter = 0.8 + Math.random() * 0.4;
|
||
|
const actualDelay = Math.floor(delay * jitter);
|
||
|
|
||
|
// Update nextRetryAt in error context
|
||
|
const nextRetryAt = Date.now() + actualDelay;
|
||
|
platformError.context.retry!.nextRetryAt = nextRetryAt;
|
||
|
|
||
|
// Log retry attempt
|
||
|
logger.warn(`Retrying operation after error (attempt ${attempt + 1}/${maxAttempts}): ${platformError.message}`, {
|
||
|
error_code: platformError.code,
|
||
|
retry_attempt: attempt + 1,
|
||
|
retry_max_attempts: maxAttempts,
|
||
|
retry_delay_ms: actualDelay,
|
||
|
retry_next_at: new Date(nextRetryAt).toISOString()
|
||
|
});
|
||
|
|
||
|
// Call onRetry callback
|
||
|
onRetry(platformError, attempt + 1, actualDelay);
|
||
|
|
||
|
// Wait before next retry
|
||
|
await new Promise(resolve => setTimeout(resolve, actualDelay));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// This should never happen, but TypeScript needs it
|
||
|
throw lastError!;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a middleware for handling errors in HTTP requests
|
||
|
*
|
||
|
* @returns Middleware function
|
||
|
*/
|
||
|
export function createErrorHandlerMiddleware() {
|
||
|
return (error: any, req: any, res: any, next: any) => {
|
||
|
// Add request context
|
||
|
const context: IErrorContext = {
|
||
|
requestId: req.headers['x-request-id'] || req.headers['x-correlation-id'],
|
||
|
component: 'HttpServer',
|
||
|
operation: `${req.method} ${req.url}`,
|
||
|
data: {
|
||
|
method: req.method,
|
||
|
url: req.url,
|
||
|
query: req.query,
|
||
|
params: req.params,
|
||
|
ip: req.ip || req.connection.remoteAddress,
|
||
|
userAgent: req.headers['user-agent']
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Handle the error
|
||
|
const formattedError = ErrorHandler.handleError(
|
||
|
error,
|
||
|
'PLATFORM_OPERATION_ERROR',
|
||
|
context
|
||
|
);
|
||
|
|
||
|
// Set status code based on error type
|
||
|
let statusCode = 500;
|
||
|
|
||
|
if (error instanceof PlatformError) {
|
||
|
// Map error categories to HTTP status codes
|
||
|
switch (error.category) {
|
||
|
case ErrorCategory.VALIDATION:
|
||
|
statusCode = 400;
|
||
|
break;
|
||
|
case ErrorCategory.AUTHENTICATION:
|
||
|
statusCode = 401;
|
||
|
break;
|
||
|
case ErrorCategory.RESOURCE:
|
||
|
statusCode = 429;
|
||
|
break;
|
||
|
case ErrorCategory.OPERATION:
|
||
|
statusCode = 400;
|
||
|
break;
|
||
|
default:
|
||
|
statusCode = 500;
|
||
|
}
|
||
|
} else if (error.statusCode) {
|
||
|
// Use provided status code if available
|
||
|
statusCode = error.statusCode;
|
||
|
}
|
||
|
|
||
|
// Send error response
|
||
|
res.status(statusCode).json({
|
||
|
success: false,
|
||
|
error: formattedError
|
||
|
});
|
||
|
};
|
||
|
}
|