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.

182 lines (158 loc) 5.08 kB
import { AxiosError, AxiosResponse, AxiosRequestConfig } from 'axios'; /** * Sensitive field patterns that should be redacted from logs */ const SENSITIVE_FIELDS = [ /key/i, /token/i, /secret/i, /password/i, /auth/i, /credential/i, /session/i, /jwt/i, /signature/i, /private/i, ]; /** * Safe replacement for sensitive data */ const REDACTED = '[REDACTED]'; /** * Maximum size for data objects in error logs to prevent excessive memory usage */ const MAX_ERROR_SIZE = { STRING: 1000, // characters ARRAY: 50, // items OBJECT: 20, // properties }; /** * Clean sensitive data from headers */ export function sanitizeHeaders(headers: Record<string, any> = {}): Record<string, any> { const sanitized: Record<string, any> = {}; Object.entries(headers).forEach(([key, value]) => { if (SENSITIVE_FIELDS.some(pattern => pattern.test(key))) { if (typeof value === 'string' && value.length > 8) { // Mask but show first and last 4 chars of long strings sanitized[key] = `${value.substring(0, 4)}...${value.substring(value.length - 4)}`; } else { sanitized[key] = REDACTED; } } else { sanitized[key] = value; } }); return sanitized; } /** * Truncate strings, arrays and objects to prevent excessive logging */ export function truncateData(data: any, depth: number = 0): any { if (depth > 3) { return '[Nested Object]'; } if (data === null || data === undefined) { return data; } if (typeof data === 'string') { if (data.length > MAX_ERROR_SIZE.STRING) { return `${data.substring(0, MAX_ERROR_SIZE.STRING)}... [truncated ${data.length - MAX_ERROR_SIZE.STRING} chars]`; } return data; } if (typeof data === 'object') { if (Array.isArray(data)) { if (data.length > MAX_ERROR_SIZE.ARRAY) { return [...data.slice(0, MAX_ERROR_SIZE.ARRAY).map(item => truncateData(item, depth + 1)), `[...${data.length - MAX_ERROR_SIZE.ARRAY} more items]`]; } return data.map(item => truncateData(item, depth + 1)); } const entries = Object.entries(data); if (entries.length > MAX_ERROR_SIZE.OBJECT) { const truncated: Record<string, any> = {}; entries.slice(0, MAX_ERROR_SIZE.OBJECT).forEach(([key, value]) => { truncated[key] = truncateData(value, depth + 1); }); truncated['[truncated]'] = `${entries.length - MAX_ERROR_SIZE.OBJECT} more properties`; return truncated; } const sanitized: Record<string, any> = {}; entries.forEach(([key, value]) => { if (SENSITIVE_FIELDS.some(pattern => pattern.test(key))) { sanitized[key] = REDACTED; } else { sanitized[key] = truncateData(value, depth + 1); } }); return sanitized; } return data; } /** * Clean an Axios config object for safe logging */ export function sanitizeConfig(config: AxiosRequestConfig = {}): AxiosRequestConfig { const { headers, auth, params, data, ...rest } = config; return { ...rest, headers: headers ? sanitizeHeaders(headers as Record<string, any>) : undefined, auth: auth ? { username: auth.username, password: REDACTED } : undefined, params: params ? truncateData(params) : undefined, data: data ? truncateData(data) : undefined, }; } /** * Clean an Axios response object for safe logging */ export function sanitizeResponse(response?: AxiosResponse): any { if (!response) return null; const { data, headers, config, ...rest } = response; return { ...rest, headers: sanitizeHeaders(headers), config: sanitizeConfig(config), data: truncateData(data), }; } /** * Extract relevant information from a rejected Axios error */ export function parseAxiosError(error: AxiosError): Record<string, any> { const errorInfo: Record<string, any> = { name: error.name, message: error.message, code: error.code, }; if (error.response) { // Server responded with a status code outside of 2xx const { status, statusText, headers, data } = error.response; errorInfo.status = status; errorInfo.statusText = statusText; errorInfo.headers = sanitizeHeaders(headers); errorInfo.data = truncateData(data); if (error.config) { errorInfo.request = { url: error.config.url, method: error.config.method?.toUpperCase(), baseURL: error.config.baseURL, headers: sanitizeHeaders(error.config.headers as Record<string, any>), }; } } else if (error.request) { // Request was made but no response received errorInfo.request = '[Request sent, no response]'; errorInfo.type = 'network'; } else { // Error in setting up the request errorInfo.type = 'client'; } // Include any custom properties from the error Object.entries(error).forEach(([key, value]) => { if (!errorInfo[key] && !['response', 'request', 'config', 'isAxiosError'].includes(key)) { errorInfo[key] = truncateData(value); } }); return errorInfo; }