UNPKG

@vepler/http-client

Version:

A flexible and extensible API service library for making HTTP requests with built-in authentication support for bearer tokens and API keys.

326 lines (290 loc) 8.95 kB
import { AxiosResponse, AxiosError } from 'axios'; /** * Base HTTP error class for all API errors */ export class HttpError extends Error { public status: number; public statusText: string; public endpoint: string; public method: string; public url: string; public data: any; public isHttpError = true; constructor( message: string, status: number = 500, statusText: string = 'Internal Server Error', endpoint: string = '', method: string = '', url: string = '', data: any = null, ) { super(message); this.name = 'HttpError'; this.status = status; this.statusText = statusText; this.endpoint = endpoint; this.method = method; this.url = url; this.data = data; // This is needed due to extending a built-in class in TypeScript Object.setPrototypeOf(this, HttpError.prototype); } /** * Formats the error for developer debugging */ toJSON() { return { name: this.name, message: this.message, status: this.status, statusText: this.statusText, endpoint: this.endpoint, method: this.method, url: this.url, data: this.data, }; } /** * Creates an HttpError from an AxiosError */ static fromAxiosError(error: AxiosError): HttpError { if (error.response) { const { status, statusText, config, data } = error.response; const method = config?.method?.toUpperCase() || 'UNKNOWN'; const endpoint = config?.url || ''; const baseUrl = config?.baseURL || ''; const url = baseUrl ? `${baseUrl}${endpoint}` : endpoint; return new HttpError( `${status} ${statusText} - ${method} ${endpoint}`, status, statusText, endpoint, method, url, data ); } else if (error.request) { // The request was made but no response was received return new NetworkError('No response received from server', error.request); } else { // Something happened in setting up the request return new ClientError(error.message || 'Request setup error'); } } } /** * 400-499 level HTTP errors */ export class ClientError extends HttpError { constructor( message: string, status: number = 400, statusText: string = 'Bad Request', endpoint: string = '', method: string = '', url: string = '', data: any = null, ) { super(message, status, statusText, endpoint, method, url, data); this.name = 'ClientError'; Object.setPrototypeOf(this, ClientError.prototype); } } /** * 500-599 level HTTP errors */ export class ServerError extends HttpError { constructor( message: string, status: number = 500, statusText: string = 'Internal Server Error', endpoint: string = '', method: string = '', url: string = '', data: any = null, ) { super(message, status, statusText, endpoint, method, url, data); this.name = 'ServerError'; Object.setPrototypeOf(this, ServerError.prototype); } } /** * Network or connectivity errors */ export class NetworkError extends HttpError { public request: any; constructor( message: string, request: any = null, endpoint: string = '', method: string = '', url: string = '', ) { super(message, 0, 'Network Error', endpoint, method, url); this.name = 'NetworkError'; this.request = request; Object.setPrototypeOf(this, NetworkError.prototype); } toJSON() { return { ...super.toJSON(), request: this.request ? '[Request Object]' : null, }; } } /** * Authentication errors (401, 403) */ export class AuthError extends ClientError { public credentials: Record<string, string>; constructor( message: string, status: number = 401, statusText: string = 'Unauthorized', endpoint: string = '', method: string = '', url: string = '', data: any = null, credentials: Record<string, string> = {} ) { super(message, status, statusText, endpoint, method, url, data); this.name = 'AuthError'; this.credentials = AuthError.sanitizeCredentials(credentials); Object.setPrototypeOf(this, AuthError.prototype); } static sanitizeCredentials(credentials: Record<string, string>): Record<string, string> { const sanitized: Record<string, string> = {}; Object.entries(credentials).forEach(([key, value]) => { if (key.toLowerCase().includes('key') || key.toLowerCase().includes('token')) { // Mask sensitive credentials but show the first and last 4 chars if (value && value.length > 8) { sanitized[key] = `${value.substring(0, 4)}...${value.substring(value.length - 4)}`; } else if (value) { sanitized[key] = '********'; } } else { sanitized[key] = value; } }); return sanitized; } toJSON() { return { ...super.toJSON(), credentials: this.credentials, }; } } /** * Request timeout errors */ export class TimeoutError extends NetworkError { constructor( message: string = 'Request timed out', request: any = null, endpoint: string = '', method: string = '', url: string = '', ) { super(message, request, endpoint, method, url); this.name = 'TimeoutError'; Object.setPrototypeOf(this, TimeoutError.prototype); } } /** * Rate limiting errors */ export class RateLimitError extends ClientError { public retryAfter?: number; constructor( message: string, retryAfter?: number, status: number = 429, statusText: string = 'Too Many Requests', endpoint: string = '', method: string = '', url: string = '', data: any = null, ) { super(message, status, statusText, endpoint, method, url, data); this.name = 'RateLimitError'; this.retryAfter = retryAfter; Object.setPrototypeOf(this, RateLimitError.prototype); } toJSON() { return { ...super.toJSON(), retryAfter: this.retryAfter, }; } } /** * Validation errors (400 with specific validation failures) */ export class ValidationError extends ClientError { public validationErrors: Record<string, string[]>; constructor( message: string, validationErrors: Record<string, string[]> = {}, status: number = 400, statusText: string = 'Bad Request', endpoint: string = '', method: string = '', url: string = '', data: any = null, ) { super(message, status, statusText, endpoint, method, url, data); this.name = 'ValidationError'; this.validationErrors = validationErrors; Object.setPrototypeOf(this, ValidationError.prototype); } toJSON() { return { ...super.toJSON(), validationErrors: this.validationErrors, }; } } /** * Creates the appropriate error type based on HTTP status code */ export function createErrorFromResponse(response: AxiosResponse): HttpError { const { status, statusText, config, data } = response; const method = config?.method?.toUpperCase() || 'UNKNOWN'; const endpoint = config?.url || ''; const baseUrl = config?.baseURL || ''; const url = baseUrl ? `${baseUrl}${endpoint}` : endpoint; const message = `${status} ${statusText} - ${method} ${endpoint}`; // Extract credentials for auth errors const credentials: Record<string, string> = {}; if (config?.headers?.['Authorization']) { credentials['Authorization'] = config.headers['Authorization'] as string; } if (config?.headers?.['x-api-key']) { credentials['x-api-key'] = config.headers['x-api-key'] as string; } // Extract validation errors if present const validationErrors: Record<string, string[]> = {}; if (status === 400 && data?.errors && typeof data.errors === 'object') { Object.entries(data.errors).forEach(([key, values]) => { validationErrors[key] = Array.isArray(values) ? values : [String(values)]; }); } // Handle specific error types based on status code if (status === 401 || status === 403) { return new AuthError(message, status, statusText, endpoint, method, url, data, credentials); } else if (status === 429) { const retryAfter = response.headers['retry-after'] ? parseInt(response.headers['retry-after'] as string, 10) : undefined; return new RateLimitError(message, retryAfter, status, statusText, endpoint, method, url, data); } else if (status === 400 && Object.keys(validationErrors).length > 0) { return new ValidationError(message, validationErrors, status, statusText, endpoint, method, url, data); } else if (status >= 400 && status < 500) { return new ClientError(message, status, statusText, endpoint, method, url, data); } else if (status >= 500) { return new ServerError(message, status, statusText, endpoint, method, url, data); } return new HttpError(message, status, statusText, endpoint, method, url, data); }