This commit is contained in:
2025-05-08 12:46:10 +00:00
parent 7aaf8f2595
commit 8b857e3d1d
26 changed files with 5215 additions and 142 deletions

437
ts/errors/base.errors.ts Normal file
View File

@ -0,0 +1,437 @@
import { ErrorSeverity, ErrorCategory, ErrorRecoverability } from './error.codes.js';
import { logger } from '../logger.js';
// Import TLogLevel from plugins
import type { TLogLevel } from '../plugins.js';
/**
* Context information added to structured errors
*/
export interface IErrorContext {
/** Component or service where the error occurred */
component?: string;
/** Operation that was being performed */
operation?: string;
/** Unique request ID if available */
requestId?: string;
/** Error occurred at timestamp */
timestamp?: number;
/** User-visible message (safe to display to end-users) */
userMessage?: string;
/** Additional structured data for debugging */
data?: Record<string, any>;
/** Related entity IDs if applicable */
entity?: {
type: string;
id: string | number;
};
/** Stack trace (if enabled in configuration) */
stack?: string;
/** Retry information if applicable */
retry?: {
/** Maximum number of retries allowed */
maxRetries?: number;
/** Current retry count */
currentRetry?: number;
/** Next retry timestamp */
nextRetryAt?: number;
/** Delay between retries (in ms) */
retryDelay?: number;
};
}
/**
* Base class for all errors in the Platform Service
* Adds structured error information, logging, and error tracking
*/
export class PlatformError extends Error {
/** Error code identifying the specific error type */
public readonly code: string;
/** Error severity level */
public readonly severity: ErrorSeverity;
/** Error category for grouping related errors */
public readonly category: ErrorCategory;
/** Whether the error can be recovered from automatically */
public readonly recoverability: ErrorRecoverability;
/** Additional context information */
public readonly context: IErrorContext;
/**
* Creates a new PlatformError
*
* @param message Error message
* @param code Error code from error.codes.ts
* @param severity Error severity level
* @param category Error category
* @param recoverability Error recoverability indication
* @param context Additional context information
*/
constructor(
message: string,
code: string,
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
category: ErrorCategory = ErrorCategory.OTHER,
recoverability: ErrorRecoverability = ErrorRecoverability.NON_RECOVERABLE,
context: IErrorContext = {}
) {
super(message);
// Set error metadata
this.name = this.constructor.name;
this.code = code;
this.severity = severity;
this.category = category;
this.recoverability = recoverability;
// Add timestamp if not provided
this.context = {
...context,
timestamp: context.timestamp || Date.now(),
};
// Capture stack trace
Error.captureStackTrace(this, this.constructor);
// Log the error automatically unless explicitly disabled
if (!context.data?.skipLogging) {
this.logError();
}
}
/**
* Logs the error using the platform logger
*/
private logError(): void {
const logLevel = this.getLogLevelFromSeverity() as TLogLevel;
// Construct structured log entry
const logData = {
error_code: this.code,
error_name: this.name,
severity: this.severity,
category: this.category,
recoverability: this.recoverability,
...this.context
};
// Log with appropriate level
logger.log(logLevel, this.message, logData);
}
/**
* Maps severity levels to log levels
*/
private getLogLevelFromSeverity(): string {
switch (this.severity) {
case ErrorSeverity.CRITICAL:
case ErrorSeverity.HIGH:
return 'error';
case ErrorSeverity.MEDIUM:
return 'warn';
case ErrorSeverity.LOW:
return 'info';
case ErrorSeverity.INFO:
return 'debug';
default:
return 'error';
}
}
/**
* Returns a JSON representation of the error
*/
public toJSON(): Record<string, any> {
return {
name: this.name,
message: this.message,
code: this.code,
severity: this.severity,
category: this.category,
recoverability: this.recoverability,
context: this.context,
stack: process.env.NODE_ENV !== 'production' ? this.stack : undefined
};
}
/**
* Creates an instance with retry information
*
* @param maxRetries Maximum number of retries
* @param currentRetry Current retry count
* @param retryDelay Delay between retries in ms
*/
public withRetry(
maxRetries: number,
currentRetry: number = 0,
retryDelay: number = 1000
): PlatformError {
const nextRetryAt = Date.now() + retryDelay;
// Create a new instance with the same parameters but updated context
return new (this.constructor as typeof PlatformError)(
this.message,
this.code,
this.severity,
this.category,
// If we can retry, the error is at least maybe recoverable
currentRetry < maxRetries
? ErrorRecoverability.MAYBE_RECOVERABLE
: this.recoverability,
{
...this.context,
retry: {
maxRetries,
currentRetry,
nextRetryAt,
retryDelay
}
}
);
}
/**
* Checks if the error should be retried based on retry information
*/
public shouldRetry(): boolean {
const { retry } = this.context;
if (!retry) return false;
return retry.currentRetry < retry.maxRetries;
}
/**
* Returns a user-friendly message that is safe to display to end users
*/
public getUserMessage(): string {
return this.context.userMessage || 'An unexpected error occurred.';
}
}
/**
* Error class for validation errors
*/
export class ValidationError extends PlatformError {
/**
* Creates a new validation error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.LOW,
ErrorCategory.VALIDATION,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Error class for configuration errors
*/
export class ConfigurationError extends PlatformError {
/**
* Creates a new configuration error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.CONFIGURATION,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Error class for network-related errors
*/
export class NetworkError extends PlatformError {
/**
* Creates a new network error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.CONNECTIVITY,
ErrorRecoverability.MAYBE_RECOVERABLE,
context
);
}
}
/**
* Error class for resource availability errors (rate limits, quotas)
*/
export class ResourceError extends PlatformError {
/**
* Creates a new resource error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.RESOURCE,
ErrorRecoverability.MAYBE_RECOVERABLE,
context
);
}
}
/**
* Error class for authentication/authorization errors
*/
export class AuthenticationError extends PlatformError {
/**
* Creates a new authentication error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.HIGH,
ErrorCategory.AUTHENTICATION,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Error class for operation errors (API calls, processing)
*/
export class OperationError extends PlatformError {
/**
* Creates a new operation error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.MAYBE_RECOVERABLE,
context
);
}
}
/**
* Error class for critical system errors
*/
export class SystemError extends PlatformError {
/**
* Creates a new system error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.CRITICAL,
ErrorCategory.OTHER,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Helper to get the appropriate error class based on error category
*
* @param category Error category
* @returns The appropriate error class
*/
export function getErrorClassForCategory(category: ErrorCategory): any {
switch (category) {
case ErrorCategory.VALIDATION:
return ValidationError;
case ErrorCategory.CONFIGURATION:
return ConfigurationError;
case ErrorCategory.CONNECTIVITY:
return NetworkError;
case ErrorCategory.RESOURCE:
return ResourceError;
case ErrorCategory.AUTHENTICATION:
return AuthenticationError;
case ErrorCategory.OPERATION:
return OperationError;
default:
return PlatformError;
}
}

313
ts/errors/email.errors.ts Normal file
View File

@ -0,0 +1,313 @@
import {
PlatformError,
ValidationError,
NetworkError,
ResourceError,
OperationError
} from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import {
EMAIL_SERVICE_ERROR,
EMAIL_TEMPLATE_ERROR,
EMAIL_VALIDATION_ERROR,
EMAIL_SEND_ERROR,
EMAIL_RECEIVE_ERROR,
EMAIL_ATTACHMENT_ERROR,
EMAIL_PARSE_ERROR,
EMAIL_RATE_LIMIT_EXCEEDED
} from './error.codes.js';
/**
* Base class for all email service related errors
*/
export class EmailServiceError extends OperationError {
/**
* Creates a new email service error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_SERVICE_ERROR, context);
}
}
/**
* Error class for email template errors
*/
export class EmailTemplateError extends OperationError {
/**
* Creates a new email template error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_TEMPLATE_ERROR, context);
}
}
/**
* Error class for email validation errors
*/
export class EmailValidationError extends ValidationError {
/**
* Creates a new email validation error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_VALIDATION_ERROR, context);
}
}
/**
* Error class for email sending errors
*/
export class EmailSendError extends OperationError {
/**
* Creates a new email send error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_SEND_ERROR, context);
}
/**
* Creates an instance for a permanently failed send
*
* @param message Error message
* @param context Additional context
*/
public static permanent(
message: string,
context: IErrorContext = {}
): EmailSendError {
return new EmailSendError(`Permanent send failure: ${message}`, {
...context,
data: {
...context.data,
permanent: true
},
userMessage: 'The email could not be delivered due to a permanent failure.'
});
}
/**
* Creates an instance for a temporary failed send
*
* @param message Error message
* @param maxRetries Maximum number of retries
* @param currentRetry Current retry count
* @param retryDelay Delay between retries in ms
* @param context Additional context
*/
public static temporary(
message: string,
maxRetries: number = 3,
currentRetry: number = 0,
retryDelay: number = 60000,
context: IErrorContext = {}
): EmailSendError {
const error = new EmailSendError(`Temporary send failure: ${message}`, {
...context,
data: {
...context.data,
permanent: false
},
userMessage: 'The email delivery failed temporarily. It will be retried.'
});
return error.withRetry(maxRetries, currentRetry, retryDelay) as EmailSendError;
}
/**
* Check if this is a permanent send failure
*/
public isPermanent(): boolean {
return !!this.context.data?.permanent;
}
}
/**
* Error class for email receiving errors
*/
export class EmailReceiveError extends OperationError {
/**
* Creates a new email receive error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_RECEIVE_ERROR, context);
}
}
/**
* Error class for email attachment errors
*/
export class EmailAttachmentError extends ValidationError {
/**
* Creates a new email attachment error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_ATTACHMENT_ERROR, context);
}
/**
* Creates an instance for an attachment too large error
*
* @param size Attachment size in bytes
* @param maxSize Maximum allowed size in bytes
* @param filename Attachment filename
* @param context Additional context
*/
public static tooLarge(
size: number,
maxSize: number,
filename?: string,
context: IErrorContext = {}
): EmailAttachmentError {
const filenameText = filename ? ` (${filename})` : '';
return new EmailAttachmentError(
`Attachment${filenameText} size ${size} bytes exceeds maximum allowed size of ${maxSize} bytes`,
{
...context,
data: {
...context.data,
size,
maxSize,
filename
},
userMessage: `The attachment${filenameText} is too large. Maximum size is ${Math.round(maxSize / 1024 / 1024)} MB.`
}
);
}
/**
* Creates an instance for an invalid attachment type error
*
* @param contentType Attachment content type
* @param filename Attachment filename
* @param allowedTypes List of allowed content types
* @param context Additional context
*/
public static invalidType(
contentType: string,
filename: string,
allowedTypes: string[],
context: IErrorContext = {}
): EmailAttachmentError {
return new EmailAttachmentError(
`Attachment '${filename}' with content type '${contentType}' is not allowed. Allowed types: ${allowedTypes.join(', ')}`,
{
...context,
data: {
...context.data,
contentType,
filename,
allowedTypes
},
userMessage: `The attachment type ${contentType} is not allowed.`
}
);
}
}
/**
* Error class for email parsing errors
*/
export class EmailParseError extends OperationError {
/**
* Creates a new email parse error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_PARSE_ERROR, context);
}
}
/**
* Error class for email rate limit exceeded errors
*/
export class EmailRateLimitError extends ResourceError {
/**
* Creates a new email rate limit error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_RATE_LIMIT_EXCEEDED, context);
}
/**
* Creates an instance with rate limit information
*
* @param limit Rate limit
* @param remaining Remaining quota
* @param resetAt Time when the quota resets
* @param scope Rate limit scope (global, domain, user, etc.)
* @param context Additional context
*/
public static withLimitInfo(
limit: number,
remaining: number,
resetAt: Date | number,
scope: string = 'global',
context: IErrorContext = {}
): EmailRateLimitError {
const resetTime = typeof resetAt === 'number' ? new Date(resetAt) : resetAt;
const resetTimeStr = resetTime.toISOString();
return new EmailRateLimitError(
`Email rate limit exceeded: ${remaining}/${limit} remaining in ${scope} scope, resets at ${resetTimeStr}`,
{
...context,
data: {
...context.data,
limit,
remaining,
resetAt: resetTime.getTime(),
resetTimeStr,
scope
},
userMessage: `You've reached the email sending limit. Please try again later.`
}
);
}
}

412
ts/errors/error-handler.ts Normal file
View 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
});
};
}

193
ts/errors/index.ts Normal file
View File

@ -0,0 +1,193 @@
/**
* Platform Service Error System
*
* This module provides a comprehensive error handling system for the Platform Service,
* with structured error types, error codes, and consistent patterns for logging and recovery.
*/
// Export error codes and types
export * from './error.codes.js';
// Export base error classes
export * from './base.errors.js';
// Export domain-specific error classes
export * from './email.errors.js';
export * from './mta.errors.js';
export * from './reputation.errors.js';
// Export utility function to create specific error types based on the error category
import { getErrorClassForCategory } from './base.errors.js';
export { getErrorClassForCategory };
/**
* Create a typed error from a standard Error
* Useful for converting errors from external libraries or APIs
*
* @param error Standard error to convert
* @param code Error code to assign
* @param contextData Additional context data
* @returns Typed PlatformError
*/
export function fromError(
error: Error,
code: string,
contextData: Record<string, any> = {}
) {
// Import and use PlatformError
const { PlatformError } = require('./base.errors.js');
const { ErrorSeverity, ErrorCategory, ErrorRecoverability } = require('./error.codes.js');
return new PlatformError(
error.message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.NON_RECOVERABLE,
{
data: {
...contextData,
originalError: {
name: error.name,
message: error.message,
stack: error.stack
}
}
}
);
}
/**
* Determine if an error is retryable
*
* @param error Error to check
* @returns Boolean indicating if the error should be retried
*/
export function isRetryable(error: any): boolean {
// If it's our platform error, use its recoverability property
if (error && typeof error === 'object' && 'recoverability' in error) {
const { ErrorRecoverability } = require('./error.codes.js');
return error.recoverability === ErrorRecoverability.RECOVERABLE ||
error.recoverability === ErrorRecoverability.MAYBE_RECOVERABLE ||
error.recoverability === ErrorRecoverability.TRANSIENT;
}
// Check if it's a network error (these are often transient)
if (error && typeof error === 'object' && error.code) {
const networkErrors = [
'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EHOSTUNREACH',
'ENETUNREACH', 'ENOTFOUND', 'EPROTO', 'ECONNABORTED'
];
return networkErrors.includes(error.code);
}
// By default, we can't determine if the error is retryable
return false;
}
/**
* Create a wrapped version of a function that catches errors
* and converts them to typed PlatformErrors
*
* @param fn Function to wrap
* @param errorCode Default error code to use
* @param contextData Additional context data
* @returns Wrapped function
*/
export function withErrorHandling<T extends (...args: any[]) => Promise<any>>(
fn: T,
errorCode: string,
contextData: Record<string, any> = {}
): T {
return (async function(...args: Parameters<T>): Promise<ReturnType<T>> {
try {
return await fn(...args);
} catch (error) {
if (error && typeof error === 'object' && 'code' in error) {
// Already a typed error, rethrow
throw error;
}
throw fromError(
error instanceof Error ? error : new Error(String(error)),
errorCode,
{
...contextData,
fnName: fn.name,
args: args.map(arg =>
typeof arg === 'object'
? '[Object]'
: String(arg).substring(0, 100)
)
}
);
}
}) as T;
}
/**
* Retry a function with exponential backoff
*
* @param fn Function to retry
* @param options Retry options
* @returns Function result or throws after max retries
*/
export async function retry<T>(
fn: () => Promise<T>,
options: {
maxRetries?: number;
initialDelay?: number;
maxDelay?: number;
backoffFactor?: number;
retryableErrors?: Array<string | RegExp>;
} = {}
): Promise<T> {
const {
maxRetries = 3,
initialDelay = 1000,
maxDelay = 30000,
backoffFactor = 2,
retryableErrors = []
} = options;
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error
? error
: new Error(String(error));
// Check if we should retry
const shouldRetry = attempt < maxRetries && (
isRetryable(error) ||
retryableErrors.some(pattern => {
if (typeof pattern === 'string') {
return lastError.message.includes(pattern);
}
return pattern.test(lastError.message);
})
);
if (!shouldRetry) {
throw lastError;
}
// Calculate delay with exponential backoff
const delay = Math.min(initialDelay * 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);
// Wait before next retry
await new Promise(resolve => setTimeout(resolve, actualDelay));
}
}
// This should never happen, but TypeScript needs it
throw lastError!;
}

611
ts/errors/mta.errors.ts Normal file
View File

@ -0,0 +1,611 @@
import {
PlatformError,
NetworkError,
AuthenticationError,
OperationError,
ConfigurationError
} from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import {
MTA_CONNECTION_ERROR,
MTA_AUTHENTICATION_ERROR,
MTA_DELIVERY_ERROR,
MTA_CONFIGURATION_ERROR,
MTA_DNS_ERROR,
MTA_TIMEOUT_ERROR,
MTA_PROTOCOL_ERROR
} from './error.codes.js';
/**
* Base class for MTA connection errors
*/
export class MtaConnectionError extends NetworkError {
/**
* Creates a new MTA connection error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_CONNECTION_ERROR, context);
}
/**
* Creates an instance for a DNS resolution error
*
* @param hostname Hostname that failed to resolve
* @param originalError Original error
* @param context Additional context
*/
public static dnsError(
hostname: string,
originalError?: Error,
context: IErrorContext = {}
): MtaConnectionError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new MtaConnectionError(
`Failed to resolve DNS for ${hostname}${errorMsg}`,
{
...context,
data: {
...context.data,
hostname,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
},
userMessage: `Could not connect to mail server for ${hostname}.`
}
);
}
/**
* Creates an instance for a connection timeout
*
* @param hostname Hostname that timed out
* @param port Port number
* @param timeout Timeout in milliseconds
* @param context Additional context
*/
public static timeout(
hostname: string,
port: number,
timeout: number,
context: IErrorContext = {}
): MtaConnectionError {
return new MtaConnectionError(
`Connection to ${hostname}:${port} timed out after ${timeout}ms`,
{
...context,
data: {
...context.data,
hostname,
port,
timeout
},
userMessage: `Connection to mail server timed out.`
}
);
}
/**
* Creates an instance for a connection refused error
*
* @param hostname Hostname that refused connection
* @param port Port number
* @param context Additional context
*/
public static refused(
hostname: string,
port: number,
context: IErrorContext = {}
): MtaConnectionError {
return new MtaConnectionError(
`Connection to ${hostname}:${port} refused`,
{
...context,
data: {
...context.data,
hostname,
port
},
userMessage: `Connection to mail server was refused.`
}
);
}
}
/**
* Error class for MTA authentication errors
*/
export class MtaAuthenticationError extends AuthenticationError {
/**
* Creates a new MTA authentication error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_AUTHENTICATION_ERROR, context);
}
/**
* Creates an instance for invalid credentials
*
* @param hostname Hostname where authentication failed
* @param username Username that failed authentication
* @param context Additional context
*/
public static invalidCredentials(
hostname: string,
username: string,
context: IErrorContext = {}
): MtaAuthenticationError {
return new MtaAuthenticationError(
`Authentication failed for user ${username} at ${hostname}`,
{
...context,
data: {
...context.data,
hostname,
username
},
userMessage: `Authentication to mail server failed.`
}
);
}
/**
* Creates an instance for unsupported authentication method
*
* @param hostname Hostname
* @param method Authentication method that is not supported
* @param supportedMethods List of supported authentication methods
* @param context Additional context
*/
public static unsupportedMethod(
hostname: string,
method: string,
supportedMethods: string[] = [],
context: IErrorContext = {}
): MtaAuthenticationError {
return new MtaAuthenticationError(
`Authentication method ${method} not supported by ${hostname}${supportedMethods.length > 0 ? `. Supported methods: ${supportedMethods.join(', ')}` : ''}`,
{
...context,
data: {
...context.data,
hostname,
method,
supportedMethods
},
userMessage: `The mail server doesn't support the required authentication method.`
}
);
}
}
/**
* Error class for MTA delivery errors
*/
export class MtaDeliveryError extends OperationError {
/**
* Creates a new MTA delivery error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_DELIVERY_ERROR, context);
}
/**
* Creates an instance for a permanent delivery failure
*
* @param message Error message
* @param recipientAddress Recipient email address
* @param statusCode SMTP status code
* @param smtpResponse Full SMTP response
* @param context Additional context
*/
public static permanent(
message: string,
recipientAddress: string,
statusCode?: string,
smtpResponse?: string,
context: IErrorContext = {}
): MtaDeliveryError {
const statusCodeStr = statusCode ? ` (${statusCode})` : '';
return new MtaDeliveryError(
`Permanent delivery failure to ${recipientAddress}${statusCodeStr}: ${message}`,
{
...context,
data: {
...context.data,
recipientAddress,
statusCode,
smtpResponse,
permanent: true
},
userMessage: `The email could not be delivered to ${recipientAddress}.`
}
);
}
/**
* Creates an instance for a temporary delivery failure
*
* @param message Error message
* @param recipientAddress Recipient email address
* @param statusCode SMTP status code
* @param smtpResponse Full SMTP response
* @param maxRetries Maximum number of retries
* @param currentRetry Current retry count
* @param retryDelay Delay between retries in ms
* @param context Additional context
*/
public static temporary(
message: string,
recipientAddress: string,
statusCode?: string,
smtpResponse?: string,
maxRetries: number = 3,
currentRetry: number = 0,
retryDelay: number = 60000,
context: IErrorContext = {}
): MtaDeliveryError {
const statusCodeStr = statusCode ? ` (${statusCode})` : '';
const error = new MtaDeliveryError(
`Temporary delivery failure to ${recipientAddress}${statusCodeStr}: ${message}`,
{
...context,
data: {
...context.data,
recipientAddress,
statusCode,
smtpResponse,
permanent: false
},
userMessage: `The email delivery to ${recipientAddress} failed temporarily. It will be retried.`
}
);
return error.withRetry(maxRetries, currentRetry, retryDelay) as MtaDeliveryError;
}
/**
* Check if this is a permanent delivery failure
*/
public isPermanent(): boolean {
return !!this.context.data?.permanent;
}
/**
* Get the recipient address associated with this delivery error
*/
public getRecipientAddress(): string | undefined {
return this.context.data?.recipientAddress;
}
/**
* Get the SMTP status code associated with this delivery error
*/
public getStatusCode(): string | undefined {
return this.context.data?.statusCode;
}
}
/**
* Error class for MTA configuration errors
*/
export class MtaConfigurationError extends ConfigurationError {
/**
* Creates a new MTA configuration error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_CONFIGURATION_ERROR, context);
}
/**
* Creates an instance for a missing configuration value
*
* @param propertyPath Path to the missing property
* @param context Additional context
*/
public static missingConfig(
propertyPath: string,
context: IErrorContext = {}
): MtaConfigurationError {
return new MtaConfigurationError(
`Missing required configuration: ${propertyPath}`,
{
...context,
data: {
...context.data,
propertyPath
},
userMessage: `The mail server is missing required configuration.`
}
);
}
/**
* Creates an instance for an invalid configuration value
*
* @param propertyPath Path to the invalid property
* @param value Current value
* @param expectedType Expected type or format
* @param context Additional context
*/
public static invalidConfig(
propertyPath: string,
value: any,
expectedType: string,
context: IErrorContext = {}
): MtaConfigurationError {
return new MtaConfigurationError(
`Invalid configuration value for ${propertyPath}: got ${value} (${typeof value}), expected ${expectedType}`,
{
...context,
data: {
...context.data,
propertyPath,
value,
expectedType
},
userMessage: `The mail server has an invalid configuration value.`
}
);
}
}
/**
* Error class for MTA DNS errors
*/
export class MtaDnsError extends NetworkError {
/**
* Creates a new MTA DNS error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_DNS_ERROR, context);
}
/**
* Creates an instance for an MX record lookup failure
*
* @param domain Domain that failed MX lookup
* @param originalError Original error
* @param context Additional context
*/
public static mxLookupFailed(
domain: string,
originalError?: Error,
context: IErrorContext = {}
): MtaDnsError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new MtaDnsError(
`Failed to lookup MX records for ${domain}${errorMsg}`,
{
...context,
data: {
...context.data,
domain,
recordType: 'MX',
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
},
userMessage: `Could not find mail servers for ${domain}.`
}
);
}
/**
* Creates an instance for a TXT record lookup failure
*
* @param domain Domain that failed TXT lookup
* @param recordPrefix Optional record prefix (e.g., 'spf', 'dkim', 'dmarc')
* @param originalError Original error
* @param context Additional context
*/
public static txtLookupFailed(
domain: string,
recordPrefix?: string,
originalError?: Error,
context: IErrorContext = {}
): MtaDnsError {
const recordType = recordPrefix ? `${recordPrefix} TXT` : 'TXT';
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new MtaDnsError(
`Failed to lookup ${recordType} records for ${domain}${errorMsg}`,
{
...context,
data: {
...context.data,
domain,
recordType,
recordPrefix,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
},
userMessage: `Could not verify ${recordPrefix || ''} records for ${domain}.`
}
);
}
}
/**
* Error class for MTA timeout errors
*/
export class MtaTimeoutError extends NetworkError {
/**
* Creates a new MTA timeout error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_TIMEOUT_ERROR, context);
}
/**
* Creates an instance for an SMTP command timeout
*
* @param command SMTP command that timed out
* @param server Server hostname
* @param timeout Timeout in milliseconds
* @param context Additional context
*/
public static commandTimeout(
command: string,
server: string,
timeout: number,
context: IErrorContext = {}
): MtaTimeoutError {
return new MtaTimeoutError(
`SMTP command ${command} to ${server} timed out after ${timeout}ms`,
{
...context,
data: {
...context.data,
command,
server,
timeout
},
userMessage: `The mail server took too long to respond.`
}
);
}
/**
* Creates an instance for an overall transaction timeout
*
* @param server Server hostname
* @param timeout Timeout in milliseconds
* @param context Additional context
*/
public static transactionTimeout(
server: string,
timeout: number,
context: IErrorContext = {}
): MtaTimeoutError {
return new MtaTimeoutError(
`SMTP transaction with ${server} timed out after ${timeout}ms`,
{
...context,
data: {
...context.data,
server,
timeout
},
userMessage: `The mail server transaction took too long to complete.`
}
);
}
}
/**
* Error class for MTA protocol errors
*/
export class MtaProtocolError extends OperationError {
/**
* Creates a new MTA protocol error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_PROTOCOL_ERROR, context);
}
/**
* Creates an instance for an unexpected server response
*
* @param command SMTP command that received unexpected response
* @param response Unexpected response
* @param expected Expected response pattern
* @param server Server hostname
* @param context Additional context
*/
public static unexpectedResponse(
command: string,
response: string,
expected: string,
server: string,
context: IErrorContext = {}
): MtaProtocolError {
return new MtaProtocolError(
`Unexpected SMTP response from ${server} for command ${command}: got "${response}", expected "${expected}"`,
{
...context,
data: {
...context.data,
command,
response,
expected,
server
},
userMessage: `Received an unexpected response from the mail server.`
}
);
}
/**
* Creates an instance for a syntax error
*
* @param details Error details
* @param server Server hostname
* @param context Additional context
*/
public static syntaxError(
details: string,
server: string,
context: IErrorContext = {}
): MtaProtocolError {
return new MtaProtocolError(
`SMTP syntax error in communication with ${server}: ${details}`,
{
...context,
data: {
...context.data,
details,
server
},
userMessage: `There was a protocol error communicating with the mail server.`
}
);
}
}

View File

@ -0,0 +1,352 @@
import {
PlatformError,
OperationError,
ResourceError
} from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import {
REPUTATION_CHECK_ERROR,
REPUTATION_DATA_ERROR,
REPUTATION_BLOCKLIST_ERROR,
REPUTATION_UPDATE_ERROR,
WARMUP_ALLOCATION_ERROR,
WARMUP_LIMIT_EXCEEDED,
WARMUP_SCHEDULE_ERROR
} from './error.codes.js';
/**
* Base class for reputation-related errors
*/
export class ReputationError extends OperationError {
/**
* Creates a new reputation error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(message, code, context);
}
}
/**
* Error class for reputation check errors
*/
export class ReputationCheckError extends ReputationError {
/**
* Creates a new reputation check error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_CHECK_ERROR, context);
}
/**
* Creates an instance for an IP reputation check error
*
* @param ip IP address
* @param provider Reputation provider
* @param originalError Original error
* @param context Additional context
*/
public static ipCheckFailed(
ip: string,
provider: string,
originalError?: Error,
context: IErrorContext = {}
): ReputationCheckError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new ReputationCheckError(
`Failed to check reputation for IP ${ip} with provider ${provider}${errorMsg}`,
{
...context,
data: {
...context.data,
ip,
provider,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
}
}
);
}
/**
* Creates an instance for a domain reputation check error
*
* @param domain Domain
* @param provider Reputation provider
* @param originalError Original error
* @param context Additional context
*/
public static domainCheckFailed(
domain: string,
provider: string,
originalError?: Error,
context: IErrorContext = {}
): ReputationCheckError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new ReputationCheckError(
`Failed to check reputation for domain ${domain} with provider ${provider}${errorMsg}`,
{
...context,
data: {
...context.data,
domain,
provider,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
}
}
);
}
}
/**
* Error class for reputation data errors
*/
export class ReputationDataError extends ReputationError {
/**
* Creates a new reputation data error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_DATA_ERROR, context);
}
/**
* Creates an instance for a data access error
*
* @param entity Entity type (domain, ip)
* @param entityId Entity identifier
* @param operation Operation that failed (read, write, update)
* @param originalError Original error
* @param context Additional context
*/
public static dataAccessFailed(
entity: string,
entityId: string,
operation: string,
originalError?: Error,
context: IErrorContext = {}
): ReputationDataError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new ReputationDataError(
`Failed to ${operation} reputation data for ${entity} ${entityId}${errorMsg}`,
{
...context,
data: {
...context.data,
entity,
entityId,
operation,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
}
}
);
}
}
/**
* Error class for blocklist-related errors
*/
export class BlocklistError extends ReputationError {
/**
* Creates a new blocklist error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_BLOCKLIST_ERROR, context);
}
/**
* Creates an instance for an entity found on a blocklist
*
* @param entity Entity type (domain, ip)
* @param entityId Entity identifier
* @param blocklist Blocklist name
* @param reason Reason for listing (if available)
* @param context Additional context
*/
public static entityBlocked(
entity: string,
entityId: string,
blocklist: string,
reason?: string,
context: IErrorContext = {}
): BlocklistError {
const reasonText = reason ? ` (${reason})` : '';
return new BlocklistError(
`${entity.charAt(0).toUpperCase() + entity.slice(1)} ${entityId} is listed on blocklist ${blocklist}${reasonText}`,
{
...context,
data: {
...context.data,
entity,
entityId,
blocklist,
reason
},
userMessage: `The ${entity} ${entityId} is on a blocklist. This may affect email deliverability.`
}
);
}
}
/**
* Error class for reputation update errors
*/
export class ReputationUpdateError extends ReputationError {
/**
* Creates a new reputation update error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_UPDATE_ERROR, context);
}
}
/**
* Error class for IP warmup allocation errors
*/
export class WarmupAllocationError extends ReputationError {
/**
* Creates a new warmup allocation error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, WARMUP_ALLOCATION_ERROR, context);
}
/**
* Creates an instance for no available IPs
*
* @param domain Domain requesting an IP
* @param policy Allocation policy that was used
* @param context Additional context
*/
public static noAvailableIps(
domain: string,
policy: string,
context: IErrorContext = {}
): WarmupAllocationError {
return new WarmupAllocationError(
`No available IPs for domain ${domain} using ${policy} allocation policy`,
{
...context,
data: {
...context.data,
domain,
policy
},
userMessage: `No available sending IPs for ${domain}.`
}
);
}
}
/**
* Error class for IP warmup limit exceeded errors
*/
export class WarmupLimitError extends ResourceError {
/**
* Creates a new warmup limit error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, WARMUP_LIMIT_EXCEEDED, context);
}
/**
* Creates an instance for daily sending limit exceeded
*
* @param ip IP address
* @param domain Domain
* @param limit Daily limit
* @param sent Number of emails sent
* @param context Additional context
*/
public static dailyLimitExceeded(
ip: string,
domain: string,
limit: number,
sent: number,
context: IErrorContext = {}
): WarmupLimitError {
return new WarmupLimitError(
`Daily sending limit exceeded for IP ${ip} and domain ${domain}: ${sent}/${limit}`,
{
...context,
data: {
...context.data,
ip,
domain,
limit,
sent
},
userMessage: `Daily sending limit reached for ${domain}.`
}
);
}
}
/**
* Error class for IP warmup schedule errors
*/
export class WarmupScheduleError extends ReputationError {
/**
* Creates a new warmup schedule error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, WARMUP_SCHEDULE_ERROR, context);
}
}