Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel
at dev 3.1 kB view raw
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;