UNPKG

@ritas-inc/hanaqueryapi-client

Version:

TypeScript client for HANA Query API with full type safety and error handling

371 lines (325 loc) 9.23 kB
/** * HANA Query API Client - Error Classes * * Custom error classes for different types of client errors. */ import type { ClientErrorType, ClientErrorDetails, ProblemDetails, RequestContext } from './types.ts'; /** * Base client error class */ export class HanaQueryClientError extends Error { public readonly type: ClientErrorType; public readonly statusCode?: number; public readonly originalError?: Error; public readonly context?: RequestContext; public readonly problemDetails?: ProblemDetails; constructor(details: ClientErrorDetails) { super(details.message); this.name = 'HanaQueryClientError'; this.type = details.type; this.statusCode = details.statusCode; this.originalError = details.originalError; this.context = details.context; this.problemDetails = details.problemDetails; // Maintains proper stack trace for where our error was thrown (only available on V8) if (Error.captureStackTrace) { Error.captureStackTrace(this, HanaQueryClientError); } } /** * Convert error to JSON for logging/serialization */ toJSON(): Record<string, any> { return { name: this.name, message: this.message, type: this.type, statusCode: this.statusCode, context: this.context, problemDetails: this.problemDetails, stack: this.stack }; } /** * Get a human-readable description of the error */ getDescription(): string { const parts: string[] = [this.message]; if (this.statusCode) { parts.push(`(HTTP ${this.statusCode})`); } if (this.context) { const duration = this.context.duration ? `${this.context.duration}ms` : 'unknown'; parts.push(`[${this.context.method} ${this.context.url}] - ${duration}`); } return parts.join(' '); } } /** * Network-related errors (connection failures, timeouts, etc.) */ export class NetworkError extends HanaQueryClientError { constructor(message: string, originalError?: Error, context?: RequestContext) { super({ type: 'network_error', message: `Network error: ${message}`, originalError, context }); this.name = 'NetworkError'; } } /** * Request timeout errors */ export class TimeoutError extends HanaQueryClientError { constructor(timeout: number, context?: RequestContext) { super({ type: 'timeout_error', message: `Request timed out after ${timeout}ms`, context }); this.name = 'TimeoutError'; } } /** * Validation errors (invalid parameters, etc.) */ export class ValidationError extends HanaQueryClientError { constructor(message: string, issues?: string[], context?: RequestContext) { const fullMessage = issues && issues.length > 0 ? `${message}: ${issues.join(', ')}` : message; super({ type: 'validation_error', message: `Validation error: ${fullMessage}`, statusCode: 400, context }); this.name = 'ValidationError'; } } /** * Authorization errors (401, 403) */ export class AuthorizationError extends HanaQueryClientError { constructor(message: string, statusCode: number, context?: RequestContext) { super({ type: 'authorization_error', message: `Authorization error: ${message}`, statusCode, context }); this.name = 'AuthorizationError'; } } /** * Not found errors (404) */ export class NotFoundError extends HanaQueryClientError { constructor(resource: string, context?: RequestContext) { super({ type: 'not_found_error', message: `Resource not found: ${resource}`, statusCode: 404, context }); this.name = 'NotFoundError'; } } /** * Server errors (5xx) */ export class ServerError extends HanaQueryClientError { constructor(message: string, statusCode: number, problemDetails?: ProblemDetails, context?: RequestContext) { super({ type: 'server_error', message: `Server error: ${message}`, statusCode, problemDetails, context }); this.name = 'ServerError'; } } /** * Unknown/unexpected errors */ export class UnknownError extends HanaQueryClientError { constructor(message: string, originalError?: Error, context?: RequestContext) { super({ type: 'unknown_error', message: `Unknown error: ${message}`, originalError, context }); this.name = 'UnknownError'; } } /** * Create appropriate error from HTTP response */ export function createErrorFromResponse( response: Response, responseData: any, context?: RequestContext ): HanaQueryClientError { const { status, statusText } = response; // Handle API error responses with problem details if (responseData && typeof responseData === 'object' && responseData.problem) { const problem = responseData.problem as ProblemDetails; switch (status) { case 400: return new ValidationError( problem.detail || 'Bad request', problem.issues, context ); case 401: case 403: return new AuthorizationError( problem.detail || statusText || 'Unauthorized', status, context ); case 404: return new NotFoundError( problem.detail || statusText || 'Not found', context ); case 500: case 502: case 503: case 504: return new ServerError( problem.detail || statusText || 'Server error', status, problem, context ); default: return new UnknownError( problem.detail || `HTTP ${status}: ${statusText}`, undefined, context ); } } // Handle HTTP errors without API problem details switch (status) { case 400: return new ValidationError('Bad request', undefined, context); case 401: case 403: return new AuthorizationError( statusText || 'Unauthorized', status, context ); case 404: return new NotFoundError(statusText || 'Not found', context); case 500: case 502: case 503: case 504: return new ServerError( statusText || 'Server error', status, undefined, context ); default: return new UnknownError( `HTTP ${status}: ${statusText}`, undefined, context ); } } /** * Create error from network/fetch failure */ export function createNetworkError( error: Error, context?: RequestContext ): HanaQueryClientError { const message = error.message.toLowerCase(); // Check for timeout if (message.includes('timeout') || error.name === 'TimeoutError') { return new TimeoutError(context?.duration || 0, context); } // Check for network issues if ( message.includes('network') || message.includes('fetch') || message.includes('connection') || error.name === 'TypeError' || error.name === 'AbortError' ) { return new NetworkError(error.message, error, context); } // Unknown error return new UnknownError(error.message, error, context); } /** * Check if an error is retryable */ export function isRetryableError(error: HanaQueryClientError): boolean { switch (error.type) { case 'network_error': case 'timeout_error': return true; case 'server_error': // Retry 5xx errors except for 501 (Not Implemented) return error.statusCode !== 501; case 'authorization_error': case 'validation_error': case 'not_found_error': return false; case 'unknown_error': default: return false; } } /** * Get retry delay for an error (exponential backoff) */ export function getRetryDelay( attempt: number, baseDelay: number, maxDelay: number, backoffMultiplier: number = 2 ): number { const delay = baseDelay * Math.pow(backoffMultiplier, attempt - 1); return Math.min(delay, maxDelay); } /** * Type guards for specific error types */ export function isNetworkError(error: any): error is NetworkError { return error instanceof NetworkError || error?.type === 'network_error'; } export function isTimeoutError(error: any): error is TimeoutError { return error instanceof TimeoutError || error?.type === 'timeout_error'; } export function isValidationError(error: any): error is ValidationError { return error instanceof ValidationError || error?.type === 'validation_error'; } export function isAuthorizationError(error: any): error is AuthorizationError { return error instanceof AuthorizationError || error?.type === 'authorization_error'; } export function isNotFoundError(error: any): error is NotFoundError { return error instanceof NotFoundError || error?.type === 'not_found_error'; } export function isServerError(error: any): error is ServerError { return error instanceof ServerError || error?.type === 'server_error'; } export function isHanaQueryClientError(error: any): error is HanaQueryClientError { return error instanceof HanaQueryClientError || (error && typeof error.type === 'string' && error.type.endsWith('_error')); }