update
This commit is contained in:
412
ts/errors/error-handler.ts
Normal file
412
ts/errors/error-handler.ts
Normal file
@ -0,0 +1,412 @@
|
||||
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
|
||||
});
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user