fixed httpclient

This commit is contained in:
Bojan Kucera 2025-06-04 16:37:28 -04:00
parent a282dac6cd
commit 557c157228
10 changed files with 603 additions and 427 deletions

View file

@ -9,8 +9,8 @@ import type {
import {
HttpError,
TimeoutError,
HttpClientConfigSchema,
RequestConfigSchema,
validateHttpClientConfig,
validateRequestConfig,
} from './types.js';
import { RateLimiter } from './rate-limiter.js';
import { ProxyManager } from './proxy-manager.js';
@ -19,10 +19,9 @@ export class HttpClient {
private readonly config: HttpClientConfig;
private readonly rateLimiter?: RateLimiter;
private readonly logger?: Logger;
constructor(config: Partial<HttpClientConfig> = {}, logger?: Logger) {
// Validate and set default configuration
this.config = HttpClientConfigSchema.parse(config);
this.config = validateHttpClientConfig(config);
this.logger = logger;
// Initialize rate limiter if configured
@ -38,10 +37,9 @@ export class HttpClient {
/**
* Make an HTTP request
*/
async request<T = any>(config: RequestConfig): Promise<HttpResponse<T>> {
*/ async request<T = any>(config: RequestConfig): Promise<HttpResponse<T>> {
// Validate request configuration
const validatedConfig = RequestConfigSchema.parse(config);
const validatedConfig = validateRequestConfig(config);
// Merge with default configuration
const finalConfig = this.mergeConfig(validatedConfig);
@ -54,10 +52,8 @@ export class HttpClient {
this.logger?.debug('Making HTTP request', {
method: finalConfig.method,
url: finalConfig.url
});
let lastError: Error | undefined;
const maxRetries = finalConfig.retries ?? this.config.retries;
}); let lastError: Error | undefined;
const maxRetries = finalConfig.retries ?? 3;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
@ -99,7 +95,7 @@ export class HttpClient {
// Wait before retrying (except on last attempt)
if (attempt < maxRetries) {
const delay = finalConfig.retryDelay ?? this.config.retryDelay;
const delay = finalConfig.retryDelay ?? 1000;
await this.sleep(delay * Math.pow(2, attempt)); // Exponential backoff
}
}
@ -133,10 +129,9 @@ export class HttpClient {
/**
* Execute the actual HTTP request
*/
private async executeRequest<T>(config: RequestConfig): Promise<HttpResponse<T>> {
*/ private async executeRequest<T>(config: RequestConfig): Promise<HttpResponse<T>> {
const url = this.buildUrl(config.url);
const timeout = config.timeout ?? this.config.timeout;
const timeout = config.timeout ?? 30000;
// Create abort controller for timeout
const abortController = new AbortController();
@ -144,13 +139,21 @@ export class HttpClient {
abortController.abort();
}, timeout);
try {
// Prepare request options
try { // Prepare request options
const requestOptions: RequestInit = {
method: config.method,
headers: config.headers,
headers: config.headers ? {...config.headers} : {},
signal: abortController.signal,
};
// Add basic auth if provided
if (config.auth) {
const authValue = `Basic ${Buffer.from(`${config.auth.username}:${config.auth.password}`).toString('base64')}`;
requestOptions.headers = {
...requestOptions.headers,
'Authorization': authValue
};
}
// Add body for non-GET requests
if (config.body && config.method !== 'GET' && config.method !== 'HEAD') {
@ -229,31 +232,44 @@ export class HttpClient {
/**
* Merge request config with default config
*/
private mergeConfig(config: RequestConfig): RequestConfig {
*/ private mergeConfig(config: RequestConfig): RequestConfig {
return {
...config,
headers: {
...this.config.defaultHeaders,
...config.headers,
},
timeout: config.timeout ?? this.config.timeout,
retries: config.retries ?? this.config.retries,
retryDelay: config.retryDelay ?? this.config.retryDelay,
validateStatus: config.validateStatus ?? this.config.validateStatus,
timeout: config.timeout ?? this.config.timeout ?? 30000,
retries: config.retries ?? this.config.retries ?? 3,
retryDelay: config.retryDelay ?? this.config.retryDelay ?? 1000,
validateStatus: config.validateStatus ?? this.config.validateStatus ?? this.defaultValidateStatus,
};
}
/**
* Build full URL from base URL and request URL
*/
private buildUrl(url: string): string {
// If it's already a full URL, return it as is
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// If we have a baseURL, combine them
if (this.config.baseURL) {
return new URL(url, this.config.baseURL).toString();
try {
// Try to use URL constructor
return new URL(url, this.config.baseURL).toString();
} catch (e) {
// Fall back to string concatenation if URL constructor fails
const base = this.config.baseURL.endsWith('/') ? this.config.baseURL : `${this.config.baseURL}/`;
const path = url.startsWith('/') ? url.substring(1) : url;
return `${base}${path}`;
}
}
// No baseURL, so prepend https:// if it's a domain-like string
if (!url.includes('://') && url.includes('.')) {
return `https://${url}`;
}
return url;