Skip to content

Commit 2625b8b

Browse files
committed
Enhance ConcurrencyQueue to support plugin error handling and update contentstackHttpClient to utilize normalized plugins. Adjust unit tests to verify plugin behavior on error responses.
1 parent bd53c2e commit 2625b8b

File tree

5 files changed

+78
-34
lines changed

5 files changed

+78
-34
lines changed

.talismanrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ fileignoreconfig:
99
ignore_detectors:
1010
- filecontent
1111
- filename: package-lock.json
12-
checksum: d6e0739053d0068a31f75ef462d9e80a68a165d7715115cc2f33cdf65d9084dd
12+
checksum: 751efa34d2f832c7b99771568b5125d929dab095784b6e4ea659daaa612994c8
1313
- filename: .husky/pre-commit
1414
checksum: 52a664f536cf5d1be0bea19cb6031ca6e8107b45b6314fe7d47b7fad7d800632
1515
- filename: test/sanity-check/api/user-test.js

lib/core/concurrency-queue.js

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const defaultConfig = {
5252
* @returns {Object} ConcurrencyQueue instance with request/response interceptors attached to Axios.
5353
* @throws {Error} If axios instance is not provided or configuration is invalid.
5454
*/
55-
export function ConcurrencyQueue ({ axios, config }) {
55+
export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
5656
if (!axios) {
5757
throw Error(ERROR_MESSAGES.AXIOS_INSTANCE_MISSING)
5858
}
@@ -436,6 +436,30 @@ export function ConcurrencyQueue ({ axios, config }) {
436436
return response
437437
}
438438

439+
// Run plugin onResponse hooks for errors so plugins see every error (including
440+
// those that are rejected without going through the plugin response interceptor).
441+
const runPluginOnResponseForError = (error) => {
442+
if (!plugins || !plugins.length) return error
443+
let currentError = error
444+
for (const plugin of plugins) {
445+
try {
446+
if (typeof plugin.onResponse === 'function') {
447+
const result = plugin.onResponse(currentError)
448+
if (result !== undefined) currentError = result
449+
}
450+
} catch (e) {
451+
if (this.config.logHandler) {
452+
this.config.logHandler('error', {
453+
name: 'PluginError',
454+
message: `Error in plugin onResponse (error handler): ${e.message}`,
455+
error: e
456+
})
457+
}
458+
}
459+
}
460+
return currentError
461+
}
462+
439463
const responseErrorHandler = error => {
440464
let networkError = error.config.retryCount
441465
let retryErrorType = null
@@ -449,7 +473,8 @@ export function ConcurrencyQueue ({ axios, config }) {
449473

450474
// Original retry logic for non-network errors
451475
if (!this.config.retryOnError || networkError > this.config.retryLimit) {
452-
return Promise.reject(responseHandler(error))
476+
const err = runPluginOnResponseForError(error)
477+
return Promise.reject(responseHandler(err))
453478
}
454479

455480
// Check rate limit remaining header before retrying
@@ -465,20 +490,23 @@ export function ConcurrencyQueue ({ axios, config }) {
465490
}
466491
response = error.response
467492
} else {
468-
return Promise.reject(responseHandler(error))
493+
const err = runPluginOnResponseForError(error)
494+
return Promise.reject(responseHandler(err))
469495
}
470496
} else if ((response.status === 401 && this.config.refreshToken)) {
471497
// If error_code is 294 (2FA required), don't retry/refresh - pass through the error as-is
472498
const apiErrorCode = response.data?.error_code
473499
if (apiErrorCode === 294) {
474-
return Promise.reject(error)
500+
const err = runPluginOnResponseForError(error)
501+
return Promise.reject(responseHandler(err))
475502
}
476503

477504
retryErrorType = `Error with status: ${response.status}`
478505
networkError++
479506

480507
if (networkError > this.config.retryLimit) {
481-
return Promise.reject(responseHandler(error))
508+
const err = runPluginOnResponseForError(error)
509+
return Promise.reject(responseHandler(err))
482510
}
483511
this.running.shift()
484512
// Cool down the running requests
@@ -492,19 +520,22 @@ export function ConcurrencyQueue ({ axios, config }) {
492520
networkError++
493521
return this.retry(error, retryErrorType, networkError, wait)
494522
}
495-
return Promise.reject(responseHandler(error))
523+
const err = runPluginOnResponseForError(error)
524+
return Promise.reject(responseHandler(err))
496525
}
497526

498527
this.retry = (error, retryErrorType, retryCount, waittime) => {
499528
let delaytime = waittime
500529
if (retryCount > this.config.retryLimit) {
501-
return Promise.reject(responseHandler(error))
530+
const err = runPluginOnResponseForError(error)
531+
return Promise.reject(responseHandler(err))
502532
}
503533
if (this.config.retryDelayOptions) {
504534
if (this.config.retryDelayOptions.customBackoff) {
505535
delaytime = this.config.retryDelayOptions.customBackoff(retryCount, error)
506536
if (delaytime && delaytime <= 0) {
507-
return Promise.reject(responseHandler(error))
537+
const err = runPluginOnResponseForError(error)
538+
return Promise.reject(responseHandler(err))
508539
}
509540
} else if (this.config.retryDelayOptions.base) {
510541
delaytime = this.config.retryDelayOptions.base * retryCount

lib/core/contentstackHTTPClient.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,11 @@ export default function contentstackHttpClient (options) {
109109
}
110110
const instance = axios.create(axiosOptions)
111111
instance.httpClientParams = options
112-
instance.concurrencyQueue = new ConcurrencyQueue({ axios: instance, config })
113112

114-
// Normalize and store plugins
115-
const plugins = normalizePlugins(config.plugins)
113+
// Normalize and store plugins before ConcurrencyQueue so plugin interceptors
114+
// run after the queue's (plugin sees responses/errors before they reach the queue).
115+
// Use options.plugins so hooks run against the same plugin references (spies work in tests).
116+
const plugins = normalizePlugins(options.plugins || config.plugins)
116117

117118
// Request interceptor for versioning strategy (must run first)
118119
instance.interceptors.request.use((request) => {
@@ -235,5 +236,7 @@ export default function contentstackHttpClient (options) {
235236
)
236237
}
237238

239+
instance.concurrencyQueue = new ConcurrencyQueue({ axios: instance, config, plugins })
240+
238241
return instance
239242
}

package-lock.json

Lines changed: 24 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/unit/ContentstackHTTPClient-test.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,13 @@ describe('Contentstack HTTP Client', () => {
251251
})
252252

253253
it('should call onResponse hook after error response', (done) => {
254-
const onResponseSpy = sinon.spy()
254+
let onResponseCalled = false
255+
let receivedError = null
255256
const plugin = {
256257
onRequest: () => {},
257258
onResponse: (error) => {
258-
onResponseSpy(error)
259+
onResponseCalled = true
260+
receivedError = error
259261
return error
260262
}
261263
}
@@ -274,12 +276,10 @@ describe('Contentstack HTTP Client', () => {
274276

275277
axiosInstance.get('/test').catch(() => {
276278
// Plugin should be called for the error
277-
// eslint-disable-next-line no-unused-expressions
278-
expect(onResponseSpy.called).to.be.true
279-
if (onResponseSpy.called) {
280-
// eslint-disable-next-line no-unused-expressions
281-
expect(onResponseSpy.calledWith(sinon.match.object)).to.be.true
282-
}
279+
expect(onResponseCalled).to.be.true
280+
expect(receivedError).to.exist
281+
expect(receivedError.response).to.exist
282+
expect(receivedError.response.status).to.equal(500)
283283
done()
284284
}).catch((err) => {
285285
// Ensure done is called even if there's an unexpected error

0 commit comments

Comments
 (0)