Skip to content

Commit 6965260

Browse files
committed
Release v1.27.5: Fix concurrency queue error handling to reject with catchable errors when response errors lack config. Update dependencies and enhance unit tests for improved error scenarios.
1 parent c01837e commit 6965260

File tree

6 files changed

+2099
-1756
lines changed

6 files changed

+2099
-1756
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: 751efa34d2f832c7b99771568b5125d929dab095784b6e4ea659daaa612994c8
12+
checksum: 93c75c1df186c336aea23c74bbefd98067d4509a42e867b0afe76e9dc65511b0
1313
- filename: .husky/pre-commit
1414
checksum: 52a664f536cf5d1be0bea19cb6031ca6e8107b45b6314fe7d47b7fad7d800632
1515
- filename: test/sanity-check/api/user-test.js

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [v1.27.5](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.5) (2026-02-11)
4+
- Fix
5+
- Concurrency queue: when response errors have no `config` (e.g. after network retries exhaust in some environments, or when plugins return a new error object), the SDK now rejects with a catchable Error instead of throwing an unhandled TypeError and crashing the process
6+
- Hardened `responseHandler` to safely handle errors without `config` (e.g. plugin-replaced errors) by guarding `config.onComplete` and still running queue `shift()` so rejections remain catchable
7+
- Added optional chaining for `error.config` reads in the retry path and unit tests for missing-config scenarios
8+
39
## [v1.27.4](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.4) (2026-02-02)
410
- Fix
511
- Removed content-type header from the release delete method

lib/core/concurrency-queue.js

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
172172
logFinalFailure(errorInfo, this.config.maxNetworkRetries)
173173
// Final error message
174174
const finalError = new Error(`Network request failed after ${this.config.maxNetworkRetries} retries: ${errorInfo.reason}`)
175-
finalError.code = error.code
175+
finalError.code = error && error.code
176176
finalError.originalError = error
177177
finalError.retryAttempts = attempt - 1
178178
return Promise.reject(finalError)
@@ -181,6 +181,16 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
181181
const delay = calculateNetworkRetryDelay(attempt)
182182
logRetryAttempt(errorInfo, attempt, delay)
183183

184+
// Guard: retry failures (e.g. from nested retries) may not have config in some
185+
// environments. Reject with a catchable error instead of throwing TypeError.
186+
if (!error || !error.config) {
187+
const finalError = new Error(`Network request failed after retries: ${errorInfo.reason}`)
188+
finalError.code = error && error.code
189+
finalError.originalError = error
190+
finalError.retryAttempts = attempt - 1
191+
return Promise.reject(finalError)
192+
}
193+
184194
// Initialize retry count if not present
185195
if (!error.config.networkRetryCount) {
186196
error.config.networkRetryCount = 0
@@ -200,7 +210,7 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
200210
safeAxiosRequest(requestConfig)
201211
.then((response) => {
202212
// On successful retry, call the original onComplete to properly clean up
203-
if (error.config.onComplete) {
213+
if (error.config && error.config.onComplete) {
204214
error.config.onComplete()
205215
}
206216
shift() // Process next queued request
@@ -214,15 +224,15 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
214224
.then(resolve)
215225
.catch((finalError) => {
216226
// On final failure, clean up the running queue
217-
if (error.config.onComplete) {
227+
if (error.config && error.config.onComplete) {
218228
error.config.onComplete()
219229
}
220230
shift() // Process next queued request
221231
reject(finalError)
222232
})
223233
} else {
224234
// On non-retryable error, clean up the running queue
225-
if (error.config.onComplete) {
235+
if (error.config && error.config.onComplete) {
226236
error.config.onComplete()
227237
}
228238
shift() // Process next queued request
@@ -429,9 +439,12 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
429439
}
430440
})
431441
}
432-
// Response interceptor used for
442+
// Response interceptor used for success and for error path (Promise.reject(responseHandler(err))).
443+
// When used with an error, err may lack config (e.g. plugin returns new error). Guard so we don't throw.
433444
const responseHandler = (response) => {
434-
response.config.onComplete()
445+
if (response?.config?.onComplete) {
446+
response.config.onComplete()
447+
}
435448
shift()
436449
return response
437450
}
@@ -461,13 +474,27 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
461474
}
462475

463476
const responseErrorHandler = error => {
464-
let networkError = error.config.retryCount
477+
// Guard: Axios errors normally have config; missing config can occur when a retry
478+
// fails in certain environments or when non-Axios errors propagate (e.g. timeouts).
479+
// Reject with a catchable error instead of throwing TypeError and crashing the process.
480+
if (!error || !error.config) {
481+
const fallbackError = new Error(
482+
error && typeof error.message === 'string'
483+
? error.message
484+
: 'Network request failed: error object missing request config'
485+
)
486+
if (error && error.code) fallbackError.code = error.code
487+
fallbackError.originalError = error
488+
return Promise.reject(runPluginOnResponseForError(fallbackError))
489+
}
490+
491+
let networkError = error?.config?.retryCount ?? 0
465492
let retryErrorType = null
466493

467494
// First, check for transient network errors
468495
const networkErrorInfo = isTransientNetworkError(error)
469496
if (networkErrorInfo && this.config.retryOnNetworkFailure) {
470-
const networkRetryCount = error.config.networkRetryCount || 0
497+
const networkRetryCount = error?.config?.networkRetryCount || 0
471498
return retryNetworkError(error, networkErrorInfo, networkRetryCount + 1)
472499
}
473500

@@ -482,7 +509,7 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
482509
var response = error.response
483510
if (!response) {
484511
if (error.code === 'ECONNABORTED') {
485-
const timeoutMs = error.config.timeout || this.config.timeout || 'unknown'
512+
const timeoutMs = error?.config?.timeout || this.config.timeout || 'unknown'
486513
error.response = {
487514
...error.response,
488515
status: 408,

0 commit comments

Comments
 (0)