import { ErrorClassification } from './types'; /** * Error types reported through the errorHandler in the client */ export enum ErrorType { NetworkUnexpectedHTTPCode, NetworkServerLimited, NetworkServerRejected, NetworkUnknown, JsonUnableToSerialize, JsonUnableToDeserialize, JsonUnknown, PluginError, InitializationError, ResetError, FlushError, EventsDropped, } /** * Segment Error object for ErrorHandler option */ export class SegmentError extends Error { type: ErrorType; message: string; innerError?: unknown; metadata?: Record; constructor( type: ErrorType, message: string, innerError?: unknown, metadata?: Record ) { super(message); Object.setPrototypeOf(this, SegmentError.prototype); this.type = type; this.message = message; this.innerError = innerError; this.metadata = metadata; } } /** * Custom Error type for Segment HTTP Error responses */ export class NetworkError extends SegmentError { statusCode: number; type: | ErrorType.NetworkServerLimited | ErrorType.NetworkServerRejected | ErrorType.NetworkUnexpectedHTTPCode | ErrorType.NetworkUnknown; constructor(statusCode: number, message: string, innerError?: unknown) { let type: ErrorType; if (statusCode === 429) { type = ErrorType.NetworkServerLimited; } else if (statusCode > 300 && statusCode < 400) { type = ErrorType.NetworkUnexpectedHTTPCode; } else if (statusCode >= 400) { type = ErrorType.NetworkServerRejected; } else { type = ErrorType.NetworkUnknown; } super(type, message, innerError); Object.setPrototypeOf(this, NetworkError.prototype); this.statusCode = statusCode; this.type = type; } } /** * Error type for JSON Serialization errors */ export class JSONError extends SegmentError { constructor( type: ErrorType.JsonUnableToDeserialize | ErrorType.JsonUnableToSerialize, message: string, innerError?: unknown ) { super(type, message, innerError); Object.setPrototypeOf(this, JSONError.prototype); } } /** * Utility method for handling HTTP fetch errors * @param response Fetch Response * @returns response if status OK, throws NetworkError for everything else */ export const checkResponseForErrors = (response: Response) => { if (!response.ok) { throw new NetworkError(response.status, response.statusText); } return response; }; /** * Converts a .fetch() error to a SegmentError object for reporting to the error handler * @param error any JS error instance * @returns a SegmentError object */ export const translateHTTPError = (error: unknown): SegmentError => { // SegmentError already if (error instanceof SegmentError) { return error; // JSON Deserialization Errors } else if (error instanceof SyntaxError) { return new JSONError( ErrorType.JsonUnableToDeserialize, error.message, error ); // HTTP Errors } else { const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error'; return new NetworkError(-1, message, error); } }; /** * Classify an HTTP status code into rate_limit, transient, or permanent. * * Precedence: * 1. statusCodeOverrides — explicit per-code overrides * 2. 429 — rate limiting (if rateLimitEnabled !== false) * 3. default4xxBehavior / default5xxBehavior — range defaults * 4. Fallback — permanent (non-retryable) */ export const classifyError = ( statusCode: number, config?: { default4xxBehavior?: 'drop' | 'retry'; default5xxBehavior?: 'drop' | 'retry'; statusCodeOverrides?: Record; rateLimitEnabled?: boolean; backoffEnabled?: boolean; } ): ErrorClassification => { const override = config?.statusCodeOverrides?.[statusCode.toString()]; if (override !== undefined) { if (override === 'retry') { // If the relevant config is disabled, treat retry overrides as permanent if (statusCode === 429 && config?.rateLimitEnabled === false) { return new ErrorClassification('permanent'); } if (statusCode !== 429 && config?.backoffEnabled === false) { return new ErrorClassification('permanent'); } return statusCode === 429 ? new ErrorClassification('rate_limit') : new ErrorClassification('transient'); } return new ErrorClassification('permanent'); } if (statusCode === 429) { return config?.rateLimitEnabled !== false ? new ErrorClassification('rate_limit') : new ErrorClassification('permanent'); } if (statusCode >= 400 && statusCode < 500) { const behavior = config?.default4xxBehavior ?? 'drop'; if (behavior === 'retry' && config?.backoffEnabled === false) { return new ErrorClassification('permanent'); } return new ErrorClassification( behavior === 'retry' ? 'transient' : 'permanent' ); } if (statusCode >= 500 && statusCode < 600) { const behavior = config?.default5xxBehavior ?? 'retry'; if (behavior === 'retry' && config?.backoffEnabled === false) { return new ErrorClassification('permanent'); } return new ErrorClassification( behavior === 'retry' ? 'transient' : 'permanent' ); } return new ErrorClassification('permanent'); }; /** * Parse Retry-After header value (seconds or HTTP-date format). * Returns delay in seconds clamped to maxRetryInterval, or undefined if invalid. */ export const parseRetryAfter = ( retryAfterValue: string | null, maxRetryInterval = 300 ): number | undefined => { if (retryAfterValue === null || retryAfterValue === '') return undefined; const asNumber = Number(retryAfterValue); if (Number.isFinite(asNumber)) { if (asNumber < 0) return undefined; return Math.min(asNumber, maxRetryInterval); } const retryDate = new Date(retryAfterValue); if (!isNaN(retryDate.getTime())) { const secondsUntil = Math.ceil((retryDate.getTime() - Date.now()) / 1000); return Math.min(Math.max(secondsUntil, 0), maxRetryInterval); } return undefined; };