Skip to content

Commit 865d3c7

Browse files
authored
Merge pull request #504 from contentstack/hotfix/DX-4429
DX | 11-02-2026 | Hotfix Release
2 parents 3a00e71 + c83894c commit 865d3c7

File tree

7 files changed

+2055
-1734
lines changed

7 files changed

+2055
-1734
lines changed

.talismanrc

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
fileignoreconfig:
2+
- filename: lib/contentstackClient.js
3+
checksum: f564f6eee5c17dc73abdeab4be226a3b37942893e149d907d2a4ef415c485c5e
24
- filename: test/unit/globalField-test.js
35
checksum: 25185e3400a12e10a043dc47502d8f30b7e1c4f2b6b4d3b8b55cdc19850c48bf
46
- filename: lib/stack/index.js
@@ -9,7 +11,9 @@ fileignoreconfig:
911
ignore_detectors:
1012
- filecontent
1113
- filename: package-lock.json
12-
checksum: 751efa34d2f832c7b99771568b5125d929dab095784b6e4ea659daaa612994c8
14+
checksum: 92b88ce00603ede68344bac6bd6bf76bdb76f1e5f5ba8d1d0c79da2b72c5ecc0
15+
- filename: test/unit/ContentstackClient-test.js
16+
checksum: 5d8519b5b93c715e911a62b4033614cc4fb3596eabf31c7216ecb4cc08604a73
1317
- filename: .husky/pre-commit
1418
checksum: 52a664f536cf5d1be0bea19cb6031ca6e8107b45b6314fe7d47b7fad7d800632
1519
- filename: test/sanity-check/api/user-test.js
@@ -26,10 +30,6 @@ fileignoreconfig:
2630
checksum: e8a32ffbbbdba2a15f3d327273f0a5b4eb33cf84cd346562596ab697125bbbc6
2731
- filename: test/sanity-check/api/bulkOperation-test.js
2832
checksum: f40a14c84ab9a194aaf830ca68e14afde2ef83496a07d4a6393d7e0bed15fb0e
29-
- filename: lib/contentstackClient.js
30-
checksum: b76ca091caa3a1b2658cd422a2d8ef3ac9996aea0aff3f982d56bb309a3d9fde
31-
- filename: test/unit/ContentstackClient-test.js
32-
checksum: 974a4f335aef025b657d139bb290233a69bed1976b947c3c674e97baffe4ce2f
3333
- filename: test/unit/ContentstackHTTPClient-test.js
3434
checksum: 4043efd843e24da9afd0272c55ef4b0432e3374b2ca12b913f1a6654df3f62be
3535
- filename: test/unit/contentstack-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 & 15 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,9 +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) {
204-
error.config.onComplete()
205-
}
213+
error?.config?.onComplete?.()
206214
shift() // Process next queued request
207215
resolve(response)
208216
})
@@ -214,17 +222,13 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
214222
.then(resolve)
215223
.catch((finalError) => {
216224
// On final failure, clean up the running queue
217-
if (error.config.onComplete) {
218-
error.config.onComplete()
219-
}
225+
error?.config?.onComplete?.()
220226
shift() // Process next queued request
221227
reject(finalError)
222228
})
223229
} else {
224230
// On non-retryable error, clean up the running queue
225-
if (error.config.onComplete) {
226-
error.config.onComplete()
227-
}
231+
error?.config?.onComplete?.()
228232
shift() // Process next queued request
229233
reject(retryError)
230234
}
@@ -429,9 +433,12 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
429433
}
430434
})
431435
}
432-
// Response interceptor used for
436+
// Response interceptor used for success and for error path (Promise.reject(responseHandler(err))).
437+
// When used with an error, err may lack config (e.g. plugin returns new error). Guard so we don't throw.
433438
const responseHandler = (response) => {
434-
response.config.onComplete()
439+
if (response?.config?.onComplete) {
440+
response.config.onComplete()
441+
}
435442
shift()
436443
return response
437444
}
@@ -461,13 +468,27 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
461468
}
462469

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

467488
// First, check for transient network errors
468489
const networkErrorInfo = isTransientNetworkError(error)
469490
if (networkErrorInfo && this.config.retryOnNetworkFailure) {
470-
const networkRetryCount = error.config.networkRetryCount || 0
491+
const networkRetryCount = error?.config?.networkRetryCount || 0
471492
return retryNetworkError(error, networkErrorInfo, networkRetryCount + 1)
472493
}
473494

@@ -482,7 +503,7 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
482503
var response = error.response
483504
if (!response) {
484505
if (error.code === 'ECONNABORTED') {
485-
const timeoutMs = error.config.timeout || this.config.timeout || 'unknown'
506+
const timeoutMs = error?.config?.timeout || this.config.timeout || 'unknown'
486507
error.response = {
487508
...error.response,
488509
status: 408,

0 commit comments

Comments
 (0)