@@ -6,14 +6,15 @@ import BatchBuffer from "./batch-buffer";
66import cache from "./cache" ;
77import {
88 API_BASE_URL ,
9+ API_TIMEOUT_MS ,
910 BUCKET_LOG_PREFIX ,
1011 FEATURE_EVENT_RATE_LIMITER_WINDOW_SIZE_MS ,
1112 FEATURES_REFETCH_MS ,
1213 loadConfig ,
1314 SDK_VERSION ,
1415 SDK_VERSION_HEADER_NAME ,
1516} from "./config" ;
16- import fetchClient from "./fetch-http-client" ;
17+ import fetchClient , { withRetry } from "./fetch-http-client" ;
1718import { subscribe as triggerOnExit } from "./flusher" ;
1819import { newRateLimiter } from "./rate-limiter" ;
1920import type {
@@ -116,6 +117,8 @@ export class BucketClient {
116117 rateLimiter : ReturnType < typeof newRateLimiter > ;
117118 offline : boolean ;
118119 configFile ?: string ;
120+ featuresFetchRetries : number ;
121+ fetchTimeoutMs : number ;
119122 } ;
120123
121124 private _initialize = once ( async ( ) => {
@@ -140,7 +143,8 @@ export class BucketClient {
140143 * @param options.batchOptions - The options for the batch buffer (optional).
141144 * @param options.featureOverrides - The feature overrides to use for the client (optional).
142145 * @param options.configFile - The path to the config file (optional).
143-
146+ * @param options.featuresFetchRetries - Number of retries for fetching features (optional, defaults to 3).
147+ * @param options.fetchTimeoutMs - Timeout for fetching features (optional, defaults to 10000ms).
144148 *
145149 * @throws An error if the options are invalid.
146150 **/
@@ -182,6 +186,20 @@ export class BucketClient {
182186 "configFile must be a string" ,
183187 ) ;
184188
189+ ok (
190+ options . featuresFetchRetries === undefined ||
191+ ( Number . isInteger ( options . featuresFetchRetries ) &&
192+ options . featuresFetchRetries >= 0 ) ,
193+ "featuresFetchRetries must be a non-negative integer" ,
194+ ) ;
195+
196+ ok (
197+ options . fetchTimeoutMs === undefined ||
198+ ( Number . isInteger ( options . fetchTimeoutMs ) &&
199+ options . fetchTimeoutMs >= 0 ) ,
200+ "fetchTimeoutMs must be a non-negative integer" ,
201+ ) ;
202+
185203 if ( ! options . configFile ) {
186204 options . configFile =
187205 ( process . env . BUCKET_CONFIG_FILE ??
@@ -266,6 +284,8 @@ export class BucketClient {
266284 typeof config . featureOverrides === "function"
267285 ? config . featureOverrides
268286 : ( ) => config . featureOverrides ,
287+ featuresFetchRetries : options . featuresFetchRetries ?? 3 ,
288+ fetchTimeoutMs : options . fetchTimeoutMs ?? API_TIMEOUT_MS ,
269289 } ;
270290
271291 if ( ( config . batchOptions ?. flushOnExit ?? true ) && ! this . _config . offline ) {
@@ -643,35 +663,45 @@ export class BucketClient {
643663 * Sends a GET request to the specified path.
644664 *
645665 * @param path - The path to send the request to.
666+ * @param retries - Optional number of retries for the request.
646667 *
647668 * @returns The response from the server.
648669 * @throws An error if the path is invalid.
649670 **/
650- private async get < TResponse > ( path : string ) {
671+ private async get < TResponse > ( path : string , retries : number = 3 ) {
651672 ok ( typeof path === "string" && path . length > 0 , "path must be a string" ) ;
652673
653674 try {
654675 const url = this . buildUrl ( path ) ;
655- const response = await this . _config . httpClient . get <
656- TResponse & { success : boolean }
657- > ( url , this . _config . headers ) ;
658-
659- this . _config . logger ?. debug ( `get request to "${ url } "` , response ) ;
660-
661- if ( ! response . ok || ! isObject ( response . body ) || ! response . body . success ) {
662- this . _config . logger ?. warn (
663- `invalid response received from server for "${ url } "` ,
664- response ,
665- ) ;
666-
667- return undefined ;
668- }
669-
670- const { success : _ , ...result } = response . body ;
671- return result as TResponse ;
676+ return await withRetry (
677+ async ( ) => {
678+ const response = await this . _config . httpClient . get <
679+ TResponse & { success : boolean }
680+ > ( url , this . _config . headers , this . _config . fetchTimeoutMs ) ;
681+
682+ this . _config . logger ?. debug ( `get request to "${ url } "` , response ) ;
683+
684+ if (
685+ ! response . ok ||
686+ ! isObject ( response . body ) ||
687+ ! response . body . success
688+ ) {
689+ this . _config . logger ?. warn (
690+ `invalid response received from server for "${ url } "` ,
691+ response ,
692+ ) ;
693+ return undefined ;
694+ }
695+ const { success : _ , ...result } = response . body ;
696+ return result as TResponse ;
697+ } ,
698+ retries ,
699+ 1000 ,
700+ 10000 ,
701+ ) ;
672702 } catch ( error ) {
673703 this . _config . logger ?. error (
674- `get request to "${ path } " failed with error` ,
704+ `get request to "${ path } " failed with error after ${ retries } retries ` ,
675705 error ,
676706 ) ;
677707 return undefined ;
@@ -849,7 +879,10 @@ export class BucketClient {
849879 this . _config . staleWarningInterval ,
850880 this . _config . logger ,
851881 async ( ) => {
852- const res = await this . get < FeaturesAPIResponse > ( "features" ) ;
882+ const res = await this . get < FeaturesAPIResponse > (
883+ "features" ,
884+ this . _config . featuresFetchRetries ,
885+ ) ;
853886
854887 if ( ! isObject ( res ) || ! Array . isArray ( res ?. features ) ) {
855888 return undefined ;
0 commit comments