| | import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; |
| |
|
| | |
| | const MAX_RETRIES = 2; |
| | const RETRY_DELAY = 1000; |
| | const FAILURE_THRESHOLD = 5; |
| | const RESET_TIMEOUT = 30000; |
| | const RATE_LIMIT_DELAY = 100; |
| |
|
| | interface CustomConfig extends InternalAxiosRequestConfig { |
| | _retryCount?: number; |
| | } |
| |
|
| | |
| | const circuitStates: Record<string, { |
| | status: 'CLOSED' | 'OPEN' | 'HALF_OPEN'; |
| | failures: number; |
| | lastFailure?: number; |
| | lastRequest?: number; |
| | }> = {}; |
| |
|
| | function getHost(url?: string): string { |
| | if (!url) return 'unknown'; |
| | try { |
| | return new URL(url).hostname; |
| | } catch { |
| | return url; |
| | } |
| | } |
| |
|
| | export const httpClient: AxiosInstance = axios.create({ |
| | timeout: 10000, |
| | headers: { |
| | 'Content-Type': 'application/json', |
| | }, |
| | }); |
| |
|
| | |
| | httpClient.interceptors.request.use( |
| | async (config) => { |
| | const host = getHost(config.url); |
| |
|
| | if (!circuitStates[host]) { |
| | circuitStates[host] = { status: 'CLOSED', failures: 0 }; |
| | } |
| |
|
| | const state = circuitStates[host]; |
| |
|
| | |
| | if (state.status === 'OPEN') { |
| | const now = Date.now(); |
| | if (now - (state.lastFailure || 0) > RESET_TIMEOUT) { |
| | state.status = 'HALF_OPEN'; |
| | } else { |
| | throw new Error(`Circuit breaker is OPEN for ${host}`); |
| | } |
| | } |
| |
|
| | |
| | const now = Date.now(); |
| | if (state.lastRequest && (now - state.lastRequest < RATE_LIMIT_DELAY)) { |
| | const waitTime = RATE_LIMIT_DELAY - (now - state.lastRequest); |
| | await new Promise(resolve => setTimeout(resolve, waitTime)); |
| | } |
| | state.lastRequest = Date.now(); |
| |
|
| | return config; |
| | }, |
| | (error) => Promise.reject(error) |
| | ); |
| |
|
| | |
| | httpClient.interceptors.response.use( |
| | (response) => { |
| | const host = getHost(response.config.url); |
| | if (circuitStates[host]) { |
| | circuitStates[host].failures = 0; |
| | circuitStates[host].status = 'CLOSED'; |
| | } |
| | return response; |
| | }, |
| | async (error) => { |
| | const config = error.config as CustomConfig; |
| | const host = getHost(config?.url); |
| |
|
| | if (!circuitStates[host]) { |
| | circuitStates[host] = { status: 'CLOSED', failures: 0 }; |
| | } |
| |
|
| | const state = circuitStates[host]; |
| |
|
| | |
| | state.failures += 1; |
| | state.lastFailure = Date.now(); |
| |
|
| | if (state.failures >= FAILURE_THRESHOLD) { |
| | state.status = 'OPEN'; |
| | } |
| |
|
| | |
| | if (config && (error.response?.status === 503 || !error.response)) { |
| | config._retryCount = config._retryCount || 0; |
| |
|
| | if (config._retryCount < MAX_RETRIES) { |
| | config._retryCount += 1; |
| | const delay = RETRY_DELAY * config._retryCount; |
| |
|
| | |
| | await new Promise((resolve) => setTimeout(resolve, delay)); |
| | return httpClient(config); |
| | } |
| | } |
| |
|
| | |
| | if (error.response?.status === 503) { |
| | error.message = 'Service temporarily unavailable'; |
| | } else if (error.code === 'ECONNABORTED') { |
| | error.message = 'Request timed out'; |
| | } |
| |
|
| | return Promise.reject(error); |
| | } |
| | ); |
| |
|