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