@@ -285,6 +285,16 @@ export interface StreamParams {
285285// HTTP Client
286286// =============================================================================
287287
288+ const RETRY_STATUSES = new Set ( [ 502 , 503 , 504 ] )
289+ const RETRY_MAX_ATTEMPTS = 3
290+ const RETRY_BASE_DELAY_MS = 500
291+ const RETRY_MAX_DELAY_MS = 4000
292+
293+ function retryDelay ( attempt : number ) : number {
294+ const ceiling = Math . min ( RETRY_BASE_DELAY_MS * 2 ** ( attempt - 1 ) , RETRY_MAX_DELAY_MS )
295+ return Math . random ( ) * ceiling
296+ }
297+
288298class HttpClient {
289299 private apiKey : string
290300 private baseUrl : string
@@ -314,62 +324,47 @@ class HttpClient {
314324 return url . toString ( )
315325 }
316326
317- async get < T > ( path : string , params ?: Record < string , unknown > ) : Promise < T > {
327+ private async request < T > ( method : 'GET' | 'POST' , path : string , body ?: unknown , params ?: Record < string , unknown > ) : Promise < T > {
318328 const url = this . buildUrl ( path , params )
329+ const init : RequestInit = {
330+ method,
331+ headers : {
332+ 'X-API-Key' : this . apiKey ,
333+ 'Content-Type' : 'application/json' ,
334+ } ,
335+ }
336+ if ( body !== undefined ) init . body = JSON . stringify ( body )
319337
320- const controller = new AbortController ( )
321- const timeoutId = setTimeout ( ( ) => controller . abort ( ) , this . timeout )
322-
323- try {
324- const response = await fetch ( url , {
325- method : 'GET' ,
326- headers : {
327- 'X-API-Key' : this . apiKey ,
328- 'Content-Type' : 'application/json' ,
329- } ,
330- signal : controller . signal ,
331- } )
332-
333- clearTimeout ( timeoutId )
338+ let lastNetworkError : Error | undefined
339+ for ( let attempt = 1 ; attempt <= RETRY_MAX_ATTEMPTS ; attempt ++ ) {
340+ const controller = new AbortController ( )
341+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , this . timeout )
342+ let response : Response | undefined
343+ let networkError : Error | undefined
334344
335- if ( ! response . ok ) {
336- const errorData = await response . json ( ) . catch ( ( ) => ( { } ) ) as APIError
337- const error : Error & { code ?: string ; status ?: number } = new Error ( errorData . error ?. message || `HTTP ${ response . status } ` )
338- error . code = errorData . error ?. code || 'unknown_error'
339- error . status = response . status
340- throw error
345+ try {
346+ response = await fetch ( url , { ...init , signal : controller . signal } )
347+ } catch ( err ) {
348+ networkError = err as Error
349+ if ( networkError . name === 'AbortError' ) {
350+ clearTimeout ( timeoutId )
351+ const error : Error & { code ?: string } = new Error ( 'Request timeout' )
352+ error . code = 'timeout'
353+ throw error
354+ }
355+ } finally {
356+ clearTimeout ( timeoutId )
341357 }
342358
343- return response . json ( )
344- } catch ( err ) {
345- clearTimeout ( timeoutId )
346- if ( ( err as Error ) . name === 'AbortError' ) {
347- const error : Error & { code ?: string } = new Error ( 'Request timeout' )
348- error . code = 'timeout'
349- throw error
359+ const transient = networkError !== undefined || ( response !== undefined && RETRY_STATUSES . has ( response . status ) )
360+ if ( attempt < RETRY_MAX_ATTEMPTS && transient ) {
361+ lastNetworkError = networkError
362+ await new Promise ( resolve => setTimeout ( resolve , retryDelay ( attempt ) ) )
363+ continue
350364 }
351- throw err
352- }
353- }
354-
355- async post < T > ( path : string , body ?: unknown , params ?: Record < string , unknown > ) : Promise < T > {
356- const url = this . buildUrl ( path , params )
357-
358- const controller = new AbortController ( )
359- const timeoutId = setTimeout ( ( ) => controller . abort ( ) , this . timeout )
360-
361- try {
362- const response = await fetch ( url , {
363- method : 'POST' ,
364- headers : {
365- 'X-API-Key' : this . apiKey ,
366- 'Content-Type' : 'application/json' ,
367- } ,
368- body : body ? JSON . stringify ( body ) : undefined ,
369- signal : controller . signal ,
370- } )
371365
372- clearTimeout ( timeoutId )
366+ if ( networkError ) throw networkError
367+ if ( ! response ) throw lastNetworkError ?? new Error ( 'No response' )
373368
374369 if ( ! response . ok ) {
375370 const errorData = await response . json ( ) . catch ( ( ) => ( { } ) ) as APIError
@@ -379,16 +374,18 @@ class HttpClient {
379374 throw error
380375 }
381376
382- return response . json ( )
383- } catch ( err ) {
384- clearTimeout ( timeoutId )
385- if ( ( err as Error ) . name === 'AbortError' ) {
386- const error : Error & { code ?: string } = new Error ( 'Request timeout' )
387- error . code = 'timeout'
388- throw error
389- }
390- throw err
377+ return response . json ( ) as Promise < T >
391378 }
379+
380+ throw lastNetworkError ?? new Error ( 'Max retries exceeded' )
381+ }
382+
383+ async get < T > ( path : string , params ?: Record < string , unknown > ) : Promise < T > {
384+ return this . request < T > ( 'GET' , path , undefined , params )
385+ }
386+
387+ async post < T > ( path : string , body ?: unknown , params ?: Record < string , unknown > ) : Promise < T > {
388+ return this . request < T > ( 'POST' , path , body , params )
392389 }
393390
394391 getStreamUrl ( path : string , params ?: Record < string , unknown > ) : string {
0 commit comments