File size: 3,686 Bytes
3d23b0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';

// Configuration for Retry and Circuit Breaker
const MAX_RETRIES = 2;
const RETRY_DELAY = 1000; // ms
const FAILURE_THRESHOLD = 5;
const RESET_TIMEOUT = 30000; // 30 seconds
const RATE_LIMIT_DELAY = 100; // 100ms minimum between requests per host

interface CustomConfig extends InternalAxiosRequestConfig {
    _retryCount?: number;
}

// Circuit Breaker & Rate Limit State
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',
    },
});

// Request Interceptor: Circuit Breaker & Rate Limit Check
httpClient.interceptors.request.use(
    async (config) => {
        const host = getHost(config.url);

        if (!circuitStates[host]) {
            circuitStates[host] = { status: 'CLOSED', failures: 0 };
        }

        const state = circuitStates[host];

        // Circuit Breaker Check
        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}`);
            }
        }

        // Rate Limit Check (Simple Delay)
        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)
);

// Response Interceptor: Retry Logic and Circuit Breaker Tracking
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];

        // Track failures for Circuit Breaker
        state.failures += 1;
        state.lastFailure = Date.now();

        if (state.failures >= FAILURE_THRESHOLD) {
            state.status = 'OPEN';
        }

        // Retry Logic for 503 or Network Errors
        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;

                // Exponential backoff
                await new Promise((resolve) => setTimeout(resolve, delay));
                return httpClient(config);
            }
        }

        // Simplified error messages
        if (error.response?.status === 503) {
            error.message = 'Service temporarily unavailable';
        } else if (error.code === 'ECONNABORTED') {
            error.message = 'Request timed out';
        }

        return Promise.reject(error);
    }
);