From f2704b132f328246bc052e4aa39b086351add0e0 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:38:11 +0800 Subject: [PATCH 1/3] fix: ACNA-4162 - add constructor options to set log level --- README.md | 37 +++++++++++++++++------ src/HttpExponentialBackoff.js | 27 ++++++++++++----- src/ProxyFetch.js | 43 ++++++++++++++------------ src/utils.js | 18 +++++------ test/HttpExponentialBackoff.test.js | 47 +++++++++++++++++++++++++++++ types.d.ts | 23 ++++++++------ 6 files changed, 140 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index a8876c0..fa3856c 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ with defaults set to max of 3 retries and initial Delay as 100ms
## FunctionsfunctionfunctionReturn the appropriate Fetch function depending on proxy settings.
numberobjectFetch Retry Options
objectobjectProxy Auth Options
Promise.<Response>
+
+
+
+### new HttpExponentialBackoff([options])
+Creates an instance of HttpExponentialBackoff
+
+
+| Param | Type | Description |
+| --- | --- | --- |
+| [options] | object | configuration options |
+| [options.logLevel] | string | the log level to use (default: process.env.LOG_LEVEL or 'info') |
+
### httpExponentialBackoff.exponentialBackoff(url, requestOptions, [retryOptions], [retryOn], [retryDelay]) ⇒ Promise.<Response>
@@ -144,18 +160,18 @@ This provides a wrapper for fetch that facilitates proxy auth authorization.
**Kind**: global class
* [ProxyFetch](#ProxyFetch)
- * [new ProxyFetch(proxyAuthOptions)](#new_ProxyFetch_new)
+ * [new ProxyFetch(proxyOptions)](#new_ProxyFetch_new)
* [.fetch(resource, options)](#ProxyFetch+fetch) ⇒ Promise.<Response>
-### new ProxyFetch(proxyAuthOptions)
+### new ProxyFetch(proxyOptions)
Initialize this class with Proxy auth options
| Param | Type | Description |
| --- | --- | --- |
-| proxyAuthOptions | [ProxyAuthOptions](#ProxyAuthOptions) | the auth options to connect with |
+| proxyOptions | [ProxyOptions](#ProxyOptions) | the auth options to connect with |
@@ -172,7 +188,7 @@ Fetch function, using the configured NTLM Auth options.
-## createFetch([proxyAuthOptions]) ⇒ function
+## createFetch([proxyOptions]) ⇒ function
Return the appropriate Fetch function depending on proxy settings.
**Kind**: global function
@@ -180,7 +196,7 @@ Return the appropriate Fetch function depending on proxy settings.
| Param | Type | Description |
| --- | --- | --- |
-| [proxyAuthOptions] | [ProxyAuthOptions](#ProxyAuthOptions) | the proxy auth options |
+| [proxyOptions] | [ProxyOptions](#ProxyOptions) | the proxy options |
@@ -207,11 +223,11 @@ Fetch Retry Options
| --- | --- | --- |
| maxRetries | number | the maximum number of retries to try (default:3) |
| initialDelayInMillis | number | the initial delay in milliseconds (default:100ms) |
-| proxy | [ProxyAuthOptions](#ProxyAuthOptions) | the (optional) proxy auth options |
+| proxy | [ProxyOptions](#ProxyOptions) | the (optional) proxy auth options |
-
+
-## ProxyAuthOptions : object
+## ProxyOptions : object
Proxy Auth Options
**Kind**: global typedef
@@ -223,6 +239,7 @@ Proxy Auth Options
| [username] | string | the username for basic auth |
| [password] | string | the password for basic auth |
| rejectUnauthorized | boolean | set to false to not reject unauthorized server certs |
+| [logLevel] | string | the log level to use (default: process.env.LOG_LEVEL or 'info') |
### Debug Logs
diff --git a/src/HttpExponentialBackoff.js b/src/HttpExponentialBackoff.js
index ee5ab3c..b8dc4c0 100644
--- a/src/HttpExponentialBackoff.js
+++ b/src/HttpExponentialBackoff.js
@@ -12,10 +12,10 @@ governing permissions and limitations under the License.
const DEFAULT_MAX_RETRIES = 3
const DEFAULT_INITIAL_DELAY_MS = 100
const loggerNamespace = '@adobe/aio-lib-core-networking:HttpExponentialBackoff'
-const logger = require('@adobe/aio-lib-core-logging')(loggerNamespace, { level: process.env.LOG_LEVEL })
+const coreLogging = require('@adobe/aio-lib-core-logging')
const { createFetch, parseRetryAfterHeader } = require('./utils')
-/* global Request, Response, ProxyAuthOptions */ // for linter
+/* global Request, Response, ProxyOptions */ // for linter
/**
* Fetch Retry Options
@@ -23,7 +23,7 @@ const { createFetch, parseRetryAfterHeader } = require('./utils')
* @typedef {object} RetryOptions
* @property {number} maxRetries the maximum number of retries to try (default:3)
* @property {number} initialDelayInMillis the initial delay in milliseconds (default:100ms)
- * @property {ProxyAuthOptions} proxy the (optional) proxy auth options
+ * @property {ProxyOptions} proxy the (optional) proxy auth options
*/
/**
@@ -32,6 +32,17 @@ const { createFetch, parseRetryAfterHeader } = require('./utils')
* with defaults set to max of 3 retries and initial Delay as 100ms
*/
class HttpExponentialBackoff {
+ /**
+ * Creates an instance of HttpExponentialBackoff
+ *
+ * @param {object} [options] configuration options
+ * @param {string} [options.logLevel] the log level to use (default: process.env.LOG_LEVEL or 'info')
+ */
+ constructor (options = {}) {
+ this.logLevel = options.logLevel || process.env.LOG_LEVEL || 'info'
+ this.logger = coreLogging(loggerNamespace, { level: this.logLevel })
+ }
+
/**
* This function will retry connecting to a url end-point, with
* exponential backoff. Returns a Promise.
@@ -102,10 +113,10 @@ class HttpExponentialBackoff {
* @private
*/
__getRetryOn (retries) {
- return function (attempt, error, response) {
+ return (attempt, error, response) => {
if (attempt < retries && (error !== null || (response.status > 499 && response.status < 600) || response.status === 429)) {
const msg = `Retrying after attempt ${attempt + 1}. failed: ${error || response.statusText}`
- logger.debug(msg)
+ this.logger.debug(msg)
return true
}
return false
@@ -120,10 +131,10 @@ class HttpExponentialBackoff {
* @private
*/
__getRetryDelay (initialDelayInMillis) {
- return function (attempt, error, response) {
+ return (attempt, error, response) => {
const timeToWait = Math.pow(2, attempt) * initialDelayInMillis // 1000, 2000, 4000
const msg = `Request will be retried after ${timeToWait} ms`
- logger.debug(msg)
+ this.logger.debug(msg)
return timeToWait
}
}
@@ -142,7 +153,7 @@ class HttpExponentialBackoff {
const retryAfter = response?.headers.get('Retry-After') || null
const timeToWait = parseRetryAfterHeader(retryAfter)
if (!isNaN(timeToWait)) {
- logger.debug(`Request will be retried after ${timeToWait} ms`)
+ this.logger.debug(`Request will be retried after ${timeToWait} ms`)
return timeToWait
}
return this.__getRetryDelay(initialDelayInMillis)(attempt, error, response)
diff --git a/src/ProxyFetch.js b/src/ProxyFetch.js
index c94378a..76cc41f 100644
--- a/src/ProxyFetch.js
+++ b/src/ProxyFetch.js
@@ -10,7 +10,7 @@ governing permissions and limitations under the License.
*/
const loggerNamespace = '@adobe/aio-lib-core-networking:ProxyFetch'
-const logger = require('@adobe/aio-lib-core-logging')(loggerNamespace, { level: process.env.LOG_LEVEL })
+const coreLogging = require('@adobe/aio-lib-core-logging')
const originalFetch = require('node-fetch')
const { codes } = require('./SDKErrors')
const { HttpProxyAgent } = require('http-proxy-agent')
@@ -42,42 +42,44 @@ class PatchedHttpsProxyAgent extends HttpsProxyAgent {
* @private
*
* @param {string} resourceUrl an endpoint url for proxyAgent selection
- * @param {ProxyAuthOptions} proxyOptions an object which contains auth information
+ * @param {ProxyOptions} proxyOptions an object which contains proxy information
+ * @param {object} logger the logger instance
* @returns {http.Agent} a http.Agent for basic auth proxy
*/
-function proxyAgent (resourceUrl, proxyAuthOptions) {
+function proxyAgent (resourceUrl, proxyOptions, logger) {
if (typeof resourceUrl !== 'string') {
throw new codes.ERROR_PROXY_FETCH_INITIALIZATION_TYPE({ sdkDetails: { resourceUrl }, messageValues: 'resourceUrl must be of type string' })
}
- const { proxyUrl, username, password, rejectUnauthorized = true } = proxyAuthOptions
- const proxyOpts = urlToHttpOptions(proxyUrl)
+ const { proxyUrl, username, password, rejectUnauthorized = true } = proxyOptions
+ const proxyHttpOptions = urlToHttpOptions(proxyUrl)
- if (!proxyOpts.auth && username && password) {
+ if (!proxyHttpOptions.auth && username && password) {
logger.debug('username and password not set in proxy url, using credentials passed in the constructor.')
- proxyOpts.auth = `${username}:${password}`
+ proxyHttpOptions.auth = `${username}:${password}`
}
- proxyOpts.rejectUnauthorized = rejectUnauthorized
+ proxyHttpOptions.rejectUnauthorized = rejectUnauthorized
if (rejectUnauthorized === false) {
logger.warn(`proxyAgent - rejectUnauthorized is set to ${rejectUnauthorized}`)
}
if (resourceUrl.startsWith('https')) {
- return new PatchedHttpsProxyAgent(proxyUrl, proxyOpts)
+ return new PatchedHttpsProxyAgent(proxyUrl, proxyHttpOptions)
} else {
- return new HttpProxyAgent(proxyUrl, proxyOpts)
+ return new HttpProxyAgent(proxyUrl, proxyHttpOptions)
}
}
/**
* Proxy Auth Options
*
- * @typedef {object} ProxyAuthOptions
+ * @typedef {object} ProxyOptions
* @property {string} proxyUrl - the proxy's url
* @property {string} [username] the username for basic auth
* @property {string} [password] the password for basic auth
* @property {boolean} rejectUnauthorized - set to false to not reject unauthorized server certs
+ * @property {string} [logLevel] the log level to use (default: process.env.LOG_LEVEL or 'info')
*/
/**
@@ -87,11 +89,14 @@ class ProxyFetch {
/**
* Initialize this class with Proxy auth options
*
- * @param {ProxyAuthOptions} proxyAuthOptions the auth options to connect with
+ * @param {ProxyOptions} proxyOptions the auth options to connect with
*/
- constructor (proxyAuthOptions = {}) {
- logger.debug(`constructor - authOptions: ${JSON.stringify(proxyAuthOptions)}`)
- const { proxyUrl } = proxyAuthOptions
+ constructor (proxyOptions = {}) {
+ this.logLevel = proxyOptions.logLevel || process.env.LOG_LEVEL || 'info'
+ this.logger = coreLogging(loggerNamespace, { level: this.logLevel })
+
+ this.logger.debug(`constructor - authOptions: ${JSON.stringify(proxyOptions)}`)
+ const { proxyUrl } = proxyOptions
const { auth } = urlToHttpOptions(proxyUrl)
if (!proxyUrl) {
@@ -100,15 +105,15 @@ class ProxyFetch {
}
if (!auth) {
- logger.debug('constructor: username or password not set, proxy is anonymous.')
+ this.logger.debug('constructor: username or password not set, proxy is anonymous.')
}
- this.authOptions = proxyAuthOptions
+ this.proxyOptions = proxyOptions
return this
}
/**
- * Fetch function, using the configured NTLM Auth options.
+ * Fetch function, using the configured fetch options, and proxy options (set in the constructor).
*
* @param {string | Request} resource - the url or Request object to fetch from
* @param {object} options - the fetch options
@@ -117,7 +122,7 @@ class ProxyFetch {
async fetch (resource, options = {}) {
return originalFetch(resource, {
...options,
- agent: proxyAgent(resource, this.authOptions)
+ agent: proxyAgent(resource, this.proxyOptions, this.logger)
})
}
}
diff --git a/src/utils.js b/src/utils.js
index 666337f..d5d8207 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -15,29 +15,29 @@ const { getProxyForUrl } = require('proxy-from-env')
const loggerNamespace = '@adobe/aio-lib-core-networking/utils'
const logger = require('@adobe/aio-lib-core-logging')(loggerNamespace, { level: process.env.LOG_LEVEL })
-/* global ProxyAuthOptions */
+/* global ProxyOptions */
/**
* Return the appropriate Fetch function depending on proxy settings.
*
- * @param {ProxyAuthOptions} [proxyAuthOptions] the proxy auth options
+ * @param {ProxyOptions} [proxyOptions] the proxy options
* @returns {Function} the Fetch API function
*/
-function createFetch (proxyAuthOptions) {
+function createFetch (proxyOptions) {
const fn = async (resource, options) => {
- // proxyAuthOptions as a parameter will override any proxy env vars
- if (!proxyAuthOptions) {
+ // proxyOptions as a parameter will override any proxy env vars
+ if (!proxyOptions) {
const proxyUrl = getProxyForUrl(resource)
if (proxyUrl) {
- proxyAuthOptions = { proxyUrl }
+ proxyOptions = { proxyUrl }
}
}
- if (proxyAuthOptions) {
- logger.debug(`createFetch: proxy url found ${proxyAuthOptions.proxyUrl} for url ${resource}`)
+ if (proxyOptions) {
+ logger.debug(`createFetch: proxy url found ${proxyOptions.proxyUrl} for url ${resource}`)
// in this closure: for fetch-retry, if we don't require it dynamically here, ProxyFetch will be an empty object
const ProxyFetch = require('./ProxyFetch')
- const proxyFetch = new ProxyFetch(proxyAuthOptions)
+ const proxyFetch = new ProxyFetch(proxyOptions)
return proxyFetch.fetch(resource, options)
} else {
logger.debug('createFetch: proxy url not found, using plain fetch')
diff --git a/test/HttpExponentialBackoff.test.js b/test/HttpExponentialBackoff.test.js
index 8d5ab29..d74e99f 100644
--- a/test/HttpExponentialBackoff.test.js
+++ b/test/HttpExponentialBackoff.test.js
@@ -153,6 +153,53 @@ test('exponentialBackoff with 3 retries on errors with default retry strategy an
const result = await fetchClient.exponentialBackoff('https://abc2.com/', { method: 'GET' }, { maxRetries: 2 })
expect(result.status).toBe(503)
expect(spy).toHaveBeenCalledTimes(2)
+ spy.mockRestore()
+})
+
+test('exponentialBackoff with 2 retries on errors with numeric Retry-After header', async () => {
+ const retrySpy = jest.spyOn(fetchClient, '__getRetryDelayWithRetryAfterHeader')
+ fetchMock.mockResponse('429 Too Many Requests', {
+ status: 429,
+ headers: {
+ 'Retry-After': '5'
+ }
+ })
+ const result = await fetchClient.exponentialBackoff('https://abc3.com/', { method: 'GET' }, { maxRetries: 2, initialDelayInMillis: 100 })
+ expect(result.status).toBe(429)
+ expect(retrySpy).toHaveBeenCalledWith(100)
+ retrySpy.mockRestore()
+})
+
+test('exponentialBackoff falls back to exponential backoff when Retry-After header is not present', async () => {
+ const retrySpy = jest.spyOn(fetchClient, '__getRetryDelayWithRetryAfterHeader')
+ const retryDelaySpy = jest.spyOn(fetchClient, '__getRetryDelay')
+ fetchMock.mockResponse('503 Service Unavailable', {
+ status: 503,
+ headers: {}
+ })
+ const result = await fetchClient.exponentialBackoff('https://abc4.com/', { method: 'GET' }, { maxRetries: 2, initialDelayInMillis: 50 })
+ expect(result.status).toBe(503)
+ expect(retrySpy).toHaveBeenCalledWith(50)
+ expect(retryDelaySpy).toHaveBeenCalled()
+ retrySpy.mockRestore()
+ retryDelaySpy.mockRestore()
+})
+
+test('exponentialBackoff falls back to exponential backoff when Retry-After header is invalid', async () => {
+ const retrySpy = jest.spyOn(fetchClient, '__getRetryDelayWithRetryAfterHeader')
+ const retryDelaySpy = jest.spyOn(fetchClient, '__getRetryDelay')
+ fetchMock.mockResponse('503 Service Unavailable', {
+ status: 503,
+ headers: {
+ 'Retry-After': 'invalid'
+ }
+ })
+ const result = await fetchClient.exponentialBackoff('https://abc5.com/', { method: 'GET' }, { maxRetries: 2, initialDelayInMillis: 50 })
+ expect(result.status).toBe(503)
+ expect(retrySpy).toHaveBeenCalledWith(50)
+ expect(retryDelaySpy).toHaveBeenCalled()
+ retrySpy.mockRestore()
+ retryDelaySpy.mockRestore()
})
test('exponential backoff with success in first attempt and custom retryOptions', async () => {
diff --git a/types.d.ts b/types.d.ts
index 4f22424..3dcec83 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -7,15 +7,18 @@
declare type RetryOptions = {
maxRetries: number;
initialDelayInMillis: number;
- proxy: ProxyAuthOptions;
+ proxy: ProxyOptions;
};
/**
- * This class provides methods to implement fetch with retries.
- * The retries use exponential backoff strategy
- * with defaults set to max of 3 retries and initial Delay as 100ms
+ * Creates an instance of HttpExponentialBackoff
+ * @param [options] - configuration options
+ * @param [options.logLevel] - the log level to use (default: process.env.LOG_LEVEL or 'info')
*/
declare class HttpExponentialBackoff {
+ constructor(options?: {
+ logLevel?: string;
+ });
/**
* This function will retry connecting to a url end-point, with
* exponential backoff. Returns a Promise.
@@ -35,20 +38,22 @@ declare class HttpExponentialBackoff {
* @property [username] - the username for basic auth
* @property [password] - the password for basic auth
* @property rejectUnauthorized - set to false to not reject unauthorized server certs
+ * @property [logLevel] - the log level to use (default: process.env.LOG_LEVEL or 'info')
*/
-declare type ProxyAuthOptions = {
+declare type ProxyOptions = {
proxyUrl: string;
username?: string;
password?: string;
rejectUnauthorized: boolean;
+ logLevel?: string;
};
/**
* Initialize this class with Proxy auth options
- * @param proxyAuthOptions - the auth options to connect with
+ * @param proxyOptions - the auth options to connect with
*/
declare class ProxyFetch {
- constructor(proxyAuthOptions: ProxyAuthOptions);
+ constructor(proxyOptions: ProxyOptions);
/**
* Fetch function, using the configured NTLM Auth options.
* @param resource - the url or Request object to fetch from
@@ -60,10 +65,10 @@ declare class ProxyFetch {
/**
* Return the appropriate Fetch function depending on proxy settings.
- * @param [proxyAuthOptions] - the proxy auth options
+ * @param [proxyOptions] - the proxy options
* @returns the Fetch API function
*/
-declare function createFetch(proxyAuthOptions?: ProxyAuthOptions): (...params: any[]) => any;
+declare function createFetch(proxyOptions?: ProxyOptions): (...params: any[]) => any;
/**
* Parse the Retry-After header
From a985abb217d7f8980ffcc3f23d710cac029760c5 Mon Sep 17 00:00:00 2001
From: Shazron Abdullah <36107+shazron@users.noreply.github.com>
Date: Thu, 13 Nov 2025 16:00:38 +0800
Subject: [PATCH 2/3] fix: for HttpExponentialBackoff, add logRetryAfterSeconds
option
This will log a warning if the Retry-After header is greater than logRetryAfterSeconds. Set to 0 to disable.
---
src/HttpExponentialBackoff.js | 17 ++++++++----
test/HttpExponentialBackoff.test.js | 42 +++++++++++++++++++++++++++++
2 files changed, 54 insertions(+), 5 deletions(-)
diff --git a/src/HttpExponentialBackoff.js b/src/HttpExponentialBackoff.js
index b8dc4c0..e17619f 100644
--- a/src/HttpExponentialBackoff.js
+++ b/src/HttpExponentialBackoff.js
@@ -37,10 +37,12 @@ class HttpExponentialBackoff {
*
* @param {object} [options] configuration options
* @param {string} [options.logLevel] the log level to use (default: process.env.LOG_LEVEL or 'info')
+ * @param {number} [options.logRetryAfterSeconds] the number of seconds after which to log a warning if the Retry-After header is greater than the number of seconds. Set to 0 to disable.
*/
constructor (options = {}) {
this.logLevel = options.logLevel || process.env.LOG_LEVEL || 'info'
this.logger = coreLogging(loggerNamespace, { level: this.logLevel })
+ this.logRetryAfterSeconds = options.logRetryAfterSeconds
}
/**
@@ -131,7 +133,7 @@ class HttpExponentialBackoff {
* @private
*/
__getRetryDelay (initialDelayInMillis) {
- return (attempt, error, response) => {
+ return (attempt, _error, _response) => {
const timeToWait = Math.pow(2, attempt) * initialDelayInMillis // 1000, 2000, 4000
const msg = `Request will be retried after ${timeToWait} ms`
this.logger.debug(msg)
@@ -151,10 +153,15 @@ class HttpExponentialBackoff {
__getRetryDelayWithRetryAfterHeader (initialDelayInMillis) {
return (attempt, error, response) => {
const retryAfter = response?.headers.get('Retry-After') || null
- const timeToWait = parseRetryAfterHeader(retryAfter)
- if (!isNaN(timeToWait)) {
- this.logger.debug(`Request will be retried after ${timeToWait} ms`)
- return timeToWait
+ const timeToWaitMs = parseRetryAfterHeader(retryAfter)
+ if (!isNaN(timeToWaitMs)) {
+ const message = `Request will be retried after ${timeToWaitMs} ms`
+ if (this.logRetryAfterSeconds && timeToWaitMs > (this.logRetryAfterSeconds * 1000)) {
+ this.logger.warn(message)
+ } else {
+ this.logger.debug(message)
+ }
+ return timeToWaitMs
}
return this.__getRetryDelay(initialDelayInMillis)(attempt, error, response)
}
diff --git a/test/HttpExponentialBackoff.test.js b/test/HttpExponentialBackoff.test.js
index d74e99f..e9ca10d 100644
--- a/test/HttpExponentialBackoff.test.js
+++ b/test/HttpExponentialBackoff.test.js
@@ -319,3 +319,45 @@ test('exponentialBackoff fetch throws', async () => {
fetchClient.exponentialBackoff('https://abc1.com/', { method: 'GET' })
).rejects.toThrow('this is a fetch error, no response is defined')
})
+
+test('exponentialBackoff with 2 retries logs warning when Retry-After exceeds logRetryAfterSeconds', async () => {
+ const fetchClientWithLogRetryAfter = new HttpExponentialBackoff({ logRetryAfterSeconds: 3 })
+ const warnSpy = jest.spyOn(fetchClientWithLogRetryAfter.logger, 'warn')
+ const debugSpy = jest.spyOn(fetchClientWithLogRetryAfter.logger, 'debug')
+
+ fetchMock.mockResponse('429 Too Many Requests', {
+ status: 429,
+ headers: {
+ 'Retry-After': '5' // 5 seconds, which is > 3 seconds threshold
+ }
+ })
+
+ const result = await fetchClientWithLogRetryAfter.exponentialBackoff('https://abc6.com/', { method: 'GET' }, { maxRetries: 2, initialDelayInMillis: 100 })
+ expect(result.status).toBe(429)
+ expect(warnSpy).toHaveBeenCalledWith('Request will be retried after 5000 ms')
+ expect(debugSpy).not.toHaveBeenCalledWith('Request will be retried after 5000 ms')
+
+ warnSpy.mockRestore()
+ debugSpy.mockRestore()
+})
+
+test('exponentialBackoff with 2 retries logs debug when Retry-After is below logRetryAfterSeconds', async () => {
+ const fetchClientWithLogRetryAfter = new HttpExponentialBackoff({ logRetryAfterSeconds: 10 })
+ const warnSpy = jest.spyOn(fetchClientWithLogRetryAfter.logger, 'warn')
+ const debugSpy = jest.spyOn(fetchClientWithLogRetryAfter.logger, 'debug')
+
+ fetchMock.mockResponse('429 Too Many Requests', {
+ status: 429,
+ headers: {
+ 'Retry-After': '5' // 5 seconds, which is < 10 seconds threshold
+ }
+ })
+
+ const result = await fetchClientWithLogRetryAfter.exponentialBackoff('https://abc7.com/', { method: 'GET' }, { maxRetries: 2, initialDelayInMillis: 100 })
+ expect(result.status).toBe(429)
+ expect(debugSpy).toHaveBeenCalledWith('Request will be retried after 5000 ms')
+ expect(warnSpy).not.toHaveBeenCalledWith('Request will be retried after 5000 ms')
+
+ warnSpy.mockRestore()
+ debugSpy.mockRestore()
+})
From d05c6fb4a210a55225aae35623e3b21a80817634 Mon Sep 17 00:00:00 2001
From: Shazron Abdullah <36107+shazron@users.noreply.github.com>
Date: Thu, 13 Nov 2025 18:02:20 +0800
Subject: [PATCH 3/3] update docs
---
README.md | 3 ++-
types.d.ts | 4 +++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index fa3856c..bdd4272 100644
--- a/README.md
+++ b/README.md
@@ -134,6 +134,7 @@ Creates an instance of HttpExponentialBackoff
| --- | --- | --- |
| [options] | object | configuration options |
| [options.logLevel] | string | the log level to use (default: process.env.LOG_LEVEL or 'info') |
+| [options.logRetryAfterSeconds] | number | the number of seconds after which to log a warning if the Retry-After header is greater than the number of seconds. Set to 0 to disable. |
@@ -176,7 +177,7 @@ Initialize this class with Proxy auth options
### proxyFetch.fetch(resource, options) ⇒ Promise.<Response>
-Fetch function, using the configured NTLM Auth options.
+Fetch function, using the configured fetch options, and proxy options (set in the constructor).
**Kind**: instance method of [ProxyFetch](#ProxyFetch)
**Returns**: Promise.<Response> - Promise object representing the http response
diff --git a/types.d.ts b/types.d.ts
index 3dcec83..50de1a9 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -14,10 +14,12 @@ declare type RetryOptions = {
* Creates an instance of HttpExponentialBackoff
* @param [options] - configuration options
* @param [options.logLevel] - the log level to use (default: process.env.LOG_LEVEL or 'info')
+ * @param [options.logRetryAfterSeconds] - the number of seconds after which to log a warning if the Retry-After header is greater than the number of seconds. Set to 0 to disable.
*/
declare class HttpExponentialBackoff {
constructor(options?: {
logLevel?: string;
+ logRetryAfterSeconds?: number;
});
/**
* This function will retry connecting to a url end-point, with
@@ -55,7 +57,7 @@ declare type ProxyOptions = {
declare class ProxyFetch {
constructor(proxyOptions: ProxyOptions);
/**
- * Fetch function, using the configured NTLM Auth options.
+ * Fetch function, using the configured fetch options, and proxy options (set in the constructor).
* @param resource - the url or Request object to fetch from
* @param options - the fetch options
* @returns Promise object representing the http response