Aethel Bot OSS repository!
aethel.xyz
bot
fun
ai
discord
discord-bot
aethel
1import logger from './logger';
2
3interface FetchOptions {
4 timeout?: number;
5 retries?: number;
6 retryDelay?: number;
7}
8
9class FetchError extends Error {
10 constructor(
11 message: string,
12 public readonly status?: number,
13 public readonly statusText?: string,
14 public readonly url?: string,
15 ) {
16 super(message);
17 this.name = 'FetchError';
18 }
19}
20
21const defaultOptions: FetchOptions = {
22 timeout: 10000,
23 retries: 3,
24 retryDelay: 1000,
25};
26
27const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
28
29const fetch = async (url: string | URL, init?: RequestInit & FetchOptions): Promise<Response> => {
30 const { timeout, retries = 3, retryDelay = 1000, ...fetchInit } = { ...defaultOptions, ...init };
31
32 const controller = new AbortController();
33 let timeoutId: NodeJS.Timeout | undefined;
34
35 if (timeout) {
36 timeoutId = setTimeout(() => controller.abort(), timeout);
37 }
38
39 let lastError: Error | undefined;
40
41 for (let attempt = 0; attempt <= retries; attempt++) {
42 try {
43 const nodeFetch = await import('node-fetch');
44
45 const response = await nodeFetch.default(url, {
46 ...fetchInit,
47 signal: controller.signal,
48 } as import('node-fetch').RequestInit);
49
50 if (timeoutId) {
51 clearTimeout(timeoutId);
52 }
53
54 logger.debug('HTTP request successful', {
55 url: url.toString(),
56 status: response.status,
57 attempt: attempt + 1,
58 });
59
60 return response as unknown as Response;
61 } catch (error) {
62 lastError = error as Error;
63
64 if (error instanceof Error) {
65 if (error.name === 'AbortError') {
66 if (timeoutId) {
67 clearTimeout(timeoutId);
68 }
69 throw new FetchError(
70 `Request timeout after ${timeout}ms`,
71 undefined,
72 undefined,
73 url.toString(),
74 );
75 }
76
77 if (
78 'status' in error &&
79 typeof error.status === 'number' &&
80 error.status >= 400 &&
81 error.status < 500
82 ) {
83 if (timeoutId) {
84 clearTimeout(timeoutId);
85 }
86 throw new FetchError(
87 `Client error: ${error.message}`,
88 error.status,
89 'statusText' in error ? (error.statusText as string) : undefined,
90 url.toString(),
91 );
92 }
93 }
94
95 if (attempt < retries) {
96 logger.warn('HTTP request failed, retrying', {
97 url: url.toString(),
98 attempt: attempt + 1,
99 maxRetries: retries,
100 error: error instanceof Error ? error.message : 'Unknown error',
101 });
102
103 await sleep(retryDelay * (attempt + 1));
104 }
105 }
106 }
107
108 clearTimeout(timeoutId);
109
110 logger.error('HTTP request failed after all retries', {
111 url: url.toString(),
112 retries,
113 error: lastError?.message || 'Unknown error',
114 });
115
116 throw new FetchError(
117 `Request failed after ${retries + 1} attempts: ${lastError?.message || 'Unknown error'}`,
118 undefined,
119 undefined,
120 url.toString(),
121 );
122};
123
124export default fetch;