import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import { createId } from '@paralleldrive/cuid2'; import { ApiFailedRequestRepository } from './api-failed-request.repository'; import { IApiClientRequestConfig } from '../../interfaces/Api-client-request-config.interface'; interface FailedRequestLog { RequestId: string; URL: string; Method: string; RequestHeaders: any; RequestBody: any; ResponseStatus: number; // Always provide a number (0 for connection failures) ResponseBody: any; // Always provide some response data ErrorMessage: string; CreatedAt: Date; } export interface ApiClientOptions { retryCount?: number; config?: AxiosRequestConfig; } export class ApiClient { private axios: AxiosInstance; private retryCount: number; private static _Repository = new ApiFailedRequestRepository(); private static _logQueue: FailedRequestLog[] = []; private static _isProcessingQueue = false; constructor(options: ApiClientOptions) { this.axios = axios.create(options.config); this.retryCount = options.retryCount ?? 3; } private static async processLogQueue(): Promise { if (this._isProcessingQueue || this._logQueue.length === 0) { return; } this._isProcessingQueue = true; try { // Process all queued logs in batches const batchSize = 10; while (this._logQueue.length > 0) { const batch = this._logQueue.splice(0, batchSize); // Process batch with individual error handling const promises = batch.map(async (logEntry) => { try { await this._Repository.create(logEntry); } catch (error) { console.error('Failed to log API failure to database:', error); // Re-queue failed logs for retry (with limit to prevent infinite loops) if ( !logEntry.hasOwnProperty('retryCount') || (logEntry as any).retryCount < 3 ) { (logEntry as any).retryCount = ((logEntry as any).retryCount || 0) + 1; this._logQueue.push(logEntry); } } }); await Promise.allSettled(promises); // Small delay between batches to prevent overwhelming the database if (this._logQueue.length > 0) { await new Promise((resolve) => setTimeout(resolve, 100)); } } } finally { this._isProcessingQueue = false; } } private static queueFailedRequestLog(logEntry: FailedRequestLog): void { this._logQueue.push(logEntry); // Process queue asynchronously without blocking the current operation setImmediate(() => { this.processLogQueue().catch((error) => { console.error('Error processing API failure log queue:', error); }); }); } async request( config: IApiClientRequestConfig, ): Promise> { let attempts = 0; let lastError: any; const { dbTransaction, ...axiosConfig } = config; while (attempts < this.retryCount) { try { return await this.axios.request(axiosConfig); } catch (error) { lastError = error; attempts++; if (attempts < this.retryCount) { await new Promise((res) => setTimeout(res, 500 * attempts)); } } } // Log only once after all retries have failed const actualHeaders = lastError?.config?.headers || lastError?.request?._header || axiosConfig.headers; ApiClient.queueFailedRequestLog({ RequestId: createId(), URL: axiosConfig.url!, Method: axiosConfig.method ?? 'GET', RequestHeaders: actualHeaders, RequestBody: axiosConfig.data ?? { message: 'No request body provided' }, // For connection failures (DNS, timeout, etc.), there's no response so we need to provide default values instead of null ResponseStatus: lastError?.response?.status ?? 0, // 0 indicates connection failure ResponseBody: lastError?.response?.data ?? { error: 'Connection failed - no response received', }, ErrorMessage: lastError?.message ?? 'Unknown error', CreatedAt: new Date(), }); throw lastError; } get(url: string, config?: IApiClientRequestConfig) { return this.request({ ...config, method: 'GET', url }); } post(url: string, data?: any, config?: IApiClientRequestConfig) { return this.request({ ...config, method: 'POST', url, data }); } put(url: string, data?: any, config?: IApiClientRequestConfig) { return this.request({ ...config, method: 'PUT', url, data }); } delete(url: string, config?: IApiClientRequestConfig) { return this.request({ ...config, method: 'DELETE', url }); } patch(url: string, data?: any, config?: IApiClientRequestConfig) { return this.request({ ...config, method: 'PATCH', url, data }); } }