Skip to content

Commit 8a55979

Browse files
Merge pull request #499 from contentstack/development
staging PR
2 parents 4e26d31 + 9f92925 commit 8a55979

File tree

8 files changed

+552
-378
lines changed

8 files changed

+552
-378
lines changed

.talismanrc

Lines changed: 4 additions & 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: 17b5bbabcc58beaa180a7fa931fc3fb407ee0e3447d47da224f60118c0a4c294
12+
checksum: 751efa34d2f832c7b99771568b5125d929dab095784b6e4ea659daaa612994c8
1313
- filename: .husky/pre-commit
1414
checksum: 52a664f536cf5d1be0bea19cb6031ca6e8107b45b6314fe7d47b7fad7d800632
1515
- filename: test/sanity-check/api/user-test.js
@@ -34,7 +34,10 @@ fileignoreconfig:
3434
checksum: 4043efd843e24da9afd0272c55ef4b0432e3374b2ca12b913f1a6654df3f62be
3535
- filename: test/unit/contentstack-test.js
3636
checksum: 2597efae3c1ab8cc173d5bf205f1c76932211f8e0eb2a16444e055d83481976c
37+
- filename: test/unit/concurrency-Queue-test.js
38+
checksum: 186438f9eb9ba4e7fd7f335dbea2afbae9ae969b7ae3ab1b517ec7a1633d255e
3739
version: "1.0"
3840

3941

4042

43+

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
# Changelog
22

3-
## [v1.27.3](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.3) (2026-02-02)
3+
## [v1.27.4](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.4) (2026-02-02)
44
- Fix
55
- Removed content-type header from the release delete method
6+
- Plugin `onResponse` hook is now called for error responses; errors handled by the concurrency queue (e.g. 5xx, retries exhausted) were previously not running plugin hooks
7+
8+
## [v1.27.3](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.3) (2026-01-21)
9+
- Fix
10+
- Skip token refresh and preserve error_code 294 when 2FA is required (error_code 294 with 401 status) to prevent error code conversion from 294 to 401
611

712
## [v1.27.2](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.2) (2026-01-12)
813
- Enhancement

lib/core/concurrency-queue.js

Lines changed: 44 additions & 7 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,14 +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)) {
497+
// If error_code is 294 (2FA required), don't retry/refresh - pass through the error as-is
498+
const apiErrorCode = response.data?.error_code
499+
if (apiErrorCode === 294) {
500+
const err = runPluginOnResponseForError(error)
501+
return Promise.reject(responseHandler(err))
502+
}
503+
471504
retryErrorType = `Error with status: ${response.status}`
472505
networkError++
473506

474507
if (networkError > this.config.retryLimit) {
475-
return Promise.reject(responseHandler(error))
508+
const err = runPluginOnResponseForError(error)
509+
return Promise.reject(responseHandler(err))
476510
}
477511
this.running.shift()
478512
// Cool down the running requests
@@ -486,19 +520,22 @@ export function ConcurrencyQueue ({ axios, config }) {
486520
networkError++
487521
return this.retry(error, retryErrorType, networkError, wait)
488522
}
489-
return Promise.reject(responseHandler(error))
523+
const err = runPluginOnResponseForError(error)
524+
return Promise.reject(responseHandler(err))
490525
}
491526

492527
this.retry = (error, retryErrorType, retryCount, waittime) => {
493528
let delaytime = waittime
494529
if (retryCount > this.config.retryLimit) {
495-
return Promise.reject(responseHandler(error))
530+
const err = runPluginOnResponseForError(error)
531+
return Promise.reject(responseHandler(err))
496532
}
497533
if (this.config.retryDelayOptions) {
498534
if (this.config.retryDelayOptions.customBackoff) {
499535
delaytime = this.config.retryDelayOptions.customBackoff(retryCount, error)
500536
if (delaytime && delaytime <= 0) {
501-
return Promise.reject(responseHandler(error))
537+
const err = runPluginOnResponseForError(error)
538+
return Promise.reject(responseHandler(err))
502539
}
503540
} else if (this.config.retryDelayOptions.base) {
504541
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
}

0 commit comments

Comments
 (0)