@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
text/typescript
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);
}