-
Notifications
You must be signed in to change notification settings - Fork 209
Expand file tree
/
Copy pathhttpClient.ts
More file actions
106 lines (92 loc) · 3.55 KB
/
httpClient.ts
File metadata and controls
106 lines (92 loc) · 3.55 KB
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
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from "axios";
import Bottleneck from "bottleneck";
import axiosRetry, { IAxiosRetryConfig } from "axios-retry";
export interface RateLimitOptions {
/** Max requests running in parallel (default = 5). Set to null for unlimited concurrency. */
maxConcurrent?: number | null;
/** Minimum gap in ms between jobs (default = 200 → ≈5 req/s) */
minTime?: number;
}
export interface RetryOptions {
/** How many attempts before we give up (default = 3) */
retries?: number;
/**
* Predicate that decides if the error is retryable.
* If omitted we retry on network errors, 429 and 5xx.
*/
retryCondition?: (err: AxiosError) => boolean;
/**
* Callback that is called when a request is retried.
* This can be used to log the retry attempt.
*/
onRetry?: (retryCount: number, err: AxiosError, config: AxiosRequestConfig) => void;
/** Reset per-attempt timeout instead of using one global timer (default = false) */
shouldResetTimeout?: boolean;
/** First back-off delay (ms) before jitter (default = 100) */
baseDelayMs?: number;
/** Max jitter added to each back-off (default = 1000) */
maxJitterMs?: number;
/** Hard ceiling for the final delay (ms). Default = 10 000 ms */
maxDelayMs?: number;
}
export interface HttpClientOptions {
/** Standard Axios settings – baseURL, headers, timeout… */
axios?: AxiosRequestConfig;
/** Concurrency / throttle settings */
rateLimit?: RateLimitOptions;
/** Exponential-back-off settings */
retry?: RetryOptions;
}
/**
* Creates an Axios instance with rate limiting and retry capabilities
* @param opts - Options for the HTTP client
* @returns An Axios instance
*/
export function createHttpClient(opts: HttpClientOptions = {}): AxiosInstance {
const instance = axios.create({
timeout: 10_000, // default timeout of 10 seconds
...opts.axios,
});
// Only use Bottleneck if maxConcurrent is not null (null means unlimited, skip rate limiting entirely)
const maxConcurrent = opts.rateLimit?.maxConcurrent ?? 5;
if (maxConcurrent !== null) {
const minTime = opts.rateLimit?.minTime ?? 200;
const limiter = new Bottleneck({ maxConcurrent, minTime });
instance.interceptors.request.use((cfg) => limiter.schedule(async () => cfg));
}
const { retries = 3, retryCondition, onRetry, baseDelayMs = 100, maxJitterMs = 1000, maxDelayMs = 10_000 } =
opts.retry ?? {};
const retryCfg: IAxiosRetryConfig = {
retries,
shouldResetTimeout: opts.retry?.shouldResetTimeout ?? false,
retryCondition:
retryCondition ??
((err) => {
const st = err.response?.status ?? 0;
return axiosRetry.isNetworkOrIdempotentRequestError(err) || st === 429 || st >= 500;
}),
retryDelay: (attempt: number, err: AxiosError) => {
const base = baseDelayMs * 2 ** (attempt - 1);
const jitter = Math.floor(Math.random() * maxJitterMs);
const h = err.response?.headers?.["retry-after"];
let retryAfter = 0;
if (h !== undefined) {
if (/^\d+$/.test(h as string)) {
// plain integer seconds
retryAfter = Number(h) * 1000;
} else {
const parsed = Date.parse(h as string);
if (!isNaN(parsed)) retryAfter = Math.max(parsed - Date.now(), 0);
}
}
const delay = retryAfter || base + jitter;
return Math.min(delay, maxDelayMs);
},
};
if (typeof onRetry === "function") {
retryCfg.onRetry = onRetry;
}
axiosRetry(instance, retryCfg);
return instance;
}
export const http = createHttpClient();