@@ -2,34 +2,89 @@ import type * as http from 'node:http'
22import type * as https from 'node:https'
33import type * as stream from 'node:stream'
44import { pipeline } from 'node:stream'
5+ import { promisify } from 'node:util'
56
67import type { Transport } from './type.ts'
78
9+ const pipelineAsync = promisify ( pipeline )
10+
811export async function request (
912 transport : Transport ,
1013 opt : https . RequestOptions ,
1114 body : Buffer | string | stream . Readable | null = null ,
1215) : Promise < http . IncomingMessage > {
1316 return new Promise < http . IncomingMessage > ( ( resolve , reject ) => {
14- const requestObj = transport . request ( opt , ( resp ) => {
15- resolve ( resp )
17+ const requestObj = transport . request ( opt , ( response ) => {
18+ resolve ( response )
1619 } )
1720
18- if ( ! body || Buffer . isBuffer ( body ) || typeof body === 'string' ) {
19- requestObj
20- . on ( 'error' , ( e : unknown ) => {
21- reject ( e )
22- } )
23- . end ( body )
21+ requestObj . on ( 'error' , reject )
2422
25- return
23+ if ( ! body || Buffer . isBuffer ( body ) || typeof body === 'string' ) {
24+ requestObj . end ( body )
25+ } else {
26+ pipelineAsync ( body , requestObj ) . catch ( reject )
2627 }
28+ } )
29+ }
30+
31+ const MAX_RETRIES = 10
32+ const EXP_BACK_OFF_BASE_DELAY = 1000 // Base delay for exponential backoff
33+ const ADDITIONAL_DELAY_FACTOR = 1.0 // to avoid synchronized retries
34+
35+ // Retryable error codes for HTTP ( ref: minio-go)
36+ export const retryHttpCodes : Record < string , boolean > = {
37+ 408 : true ,
38+ 429 : true ,
39+ 499 : true ,
40+ 500 : true ,
41+ 502 : true ,
42+ 503 : true ,
43+ 504 : true ,
44+ 520 : true ,
45+ }
46+
47+ const isHttpRetryable = ( httpResCode : number ) => {
48+ return retryHttpCodes [ httpResCode ] !== undefined
49+ }
50+
51+ const sleep = ( ms : number ) => {
52+ return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) )
53+ }
2754
28- // pump readable stream
29- pipeline ( body , requestObj , ( err ) => {
30- if ( err ) {
31- reject ( err )
55+ const getExpBackOffDelay = ( retryCount : number ) => {
56+ const backOffBy = EXP_BACK_OFF_BASE_DELAY * 2 ** retryCount
57+ const additionalDelay = Math . random ( ) * backOffBy * ADDITIONAL_DELAY_FACTOR
58+ return backOffBy + additionalDelay
59+ }
60+
61+ export async function requestWithRetry (
62+ transport : Transport ,
63+ opt : https . RequestOptions ,
64+ body : Buffer | string | stream . Readable | null = null ,
65+ maxRetries : number = MAX_RETRIES ,
66+ ) : Promise < http . IncomingMessage > {
67+ let attempt = 0
68+ while ( attempt <= maxRetries ) {
69+ try {
70+ const response = await request ( transport , opt , body )
71+ // Check if the HTTP status code is retryable
72+ if ( isHttpRetryable ( response . statusCode as number ) ) {
73+ throw new Error ( `Retryable HTTP status: ${ response . statusCode } ` ) // trigger retry attempt with calculated delay
3274 }
33- } )
34- } )
75+ return response // Success, return the raw response
76+ } catch ( err ) {
77+ attempt ++
78+
79+ if ( attempt > maxRetries ) {
80+ throw new Error ( `Request failed after ${ maxRetries } retries: ${ err } ` )
81+ }
82+ const delay = getExpBackOffDelay ( attempt )
83+ // eslint-disable-next-line no-console
84+ // console.warn( `${new Date().toLocaleString()} Retrying request (attempt ${attempt}/${maxRetries}) after ${delay}ms due to: ${err}`,)
85+ await sleep ( delay )
86+ }
87+ }
88+
89+ throw new Error ( `${ MAX_RETRIES } Retries exhausted, request failed.` )
3590}
0 commit comments