Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
fileignoreconfig:
- filename: lib/contentstackClient.js
checksum: f564f6eee5c17dc73abdeab4be226a3b37942893e149d907d2a4ef415c485c5e
- filename: test/unit/globalField-test.js
checksum: 25185e3400a12e10a043dc47502d8f30b7e1c4f2b6b4d3b8b55cdc19850c48bf
- filename: lib/stack/index.js
Expand All @@ -9,7 +11,9 @@ fileignoreconfig:
ignore_detectors:
- filecontent
- filename: package-lock.json
checksum: 751efa34d2f832c7b99771568b5125d929dab095784b6e4ea659daaa612994c8
checksum: 92b88ce00603ede68344bac6bd6bf76bdb76f1e5f5ba8d1d0c79da2b72c5ecc0
- filename: test/unit/ContentstackClient-test.js
checksum: 5d8519b5b93c715e911a62b4033614cc4fb3596eabf31c7216ecb4cc08604a73
- filename: .husky/pre-commit
checksum: 52a664f536cf5d1be0bea19cb6031ca6e8107b45b6314fe7d47b7fad7d800632
- filename: test/sanity-check/api/user-test.js
Expand All @@ -26,10 +30,6 @@ fileignoreconfig:
checksum: e8a32ffbbbdba2a15f3d327273f0a5b4eb33cf84cd346562596ab697125bbbc6
- filename: test/sanity-check/api/bulkOperation-test.js
checksum: f40a14c84ab9a194aaf830ca68e14afde2ef83496a07d4a6393d7e0bed15fb0e
- filename: lib/contentstackClient.js
checksum: b76ca091caa3a1b2658cd422a2d8ef3ac9996aea0aff3f982d56bb309a3d9fde
- filename: test/unit/ContentstackClient-test.js
checksum: 974a4f335aef025b657d139bb290233a69bed1976b947c3c674e97baffe4ce2f
- filename: test/unit/ContentstackHTTPClient-test.js
checksum: 4043efd843e24da9afd0272c55ef4b0432e3374b2ca12b913f1a6654df3f62be
- filename: test/unit/contentstack-test.js
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [v1.27.5](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.5) (2026-02-11)
- Fix
- 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
- 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
- Added optional chaining for `error.config` reads in the retry path and unit tests for missing-config scenarios

## [v1.27.4](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.4) (2026-02-02)
- Fix
- Removed content-type header from the release delete method
Expand Down
51 changes: 36 additions & 15 deletions lib/core/concurrency-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
logFinalFailure(errorInfo, this.config.maxNetworkRetries)
// Final error message
const finalError = new Error(`Network request failed after ${this.config.maxNetworkRetries} retries: ${errorInfo.reason}`)
finalError.code = error.code
finalError.code = error && error.code
finalError.originalError = error
finalError.retryAttempts = attempt - 1
return Promise.reject(finalError)
Expand All @@ -181,6 +181,16 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
const delay = calculateNetworkRetryDelay(attempt)
logRetryAttempt(errorInfo, attempt, delay)

// Guard: retry failures (e.g. from nested retries) may not have config in some
// environments. Reject with a catchable error instead of throwing TypeError.
if (!error || !error.config) {
const finalError = new Error(`Network request failed after retries: ${errorInfo.reason}`)
finalError.code = error && error.code
finalError.originalError = error
finalError.retryAttempts = attempt - 1
return Promise.reject(finalError)
}

// Initialize retry count if not present
if (!error.config.networkRetryCount) {
error.config.networkRetryCount = 0
Expand All @@ -200,9 +210,7 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
safeAxiosRequest(requestConfig)
.then((response) => {
// On successful retry, call the original onComplete to properly clean up
if (error.config.onComplete) {
error.config.onComplete()
}
error?.config?.onComplete?.()
shift() // Process next queued request
resolve(response)
})
Expand All @@ -214,17 +222,13 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
.then(resolve)
.catch((finalError) => {
// On final failure, clean up the running queue
if (error.config.onComplete) {
error.config.onComplete()
}
error?.config?.onComplete?.()
shift() // Process next queued request
reject(finalError)
})
} else {
// On non-retryable error, clean up the running queue
if (error.config.onComplete) {
error.config.onComplete()
}
error?.config?.onComplete?.()
shift() // Process next queued request
reject(retryError)
}
Expand Down Expand Up @@ -429,9 +433,12 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
}
})
}
// Response interceptor used for
// Response interceptor used for success and for error path (Promise.reject(responseHandler(err))).
// When used with an error, err may lack config (e.g. plugin returns new error). Guard so we don't throw.
const responseHandler = (response) => {
response.config.onComplete()
if (response?.config?.onComplete) {
response.config.onComplete()
}
shift()
return response
}
Expand Down Expand Up @@ -461,13 +468,27 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
}

const responseErrorHandler = error => {
let networkError = error.config.retryCount
// Guard: Axios errors normally have config; missing config can occur when a retry
// fails in certain environments or when non-Axios errors propagate (e.g. timeouts).
// Reject with a catchable error instead of throwing TypeError and crashing the process.
if (!error || !error.config) {
const fallbackError = new Error(
error && typeof error.message === 'string'
? error.message
: 'Network request failed: error object missing request config'
)
fallbackError.code = error?.code
fallbackError.originalError = error
return Promise.reject(runPluginOnResponseForError(fallbackError))
}

let networkError = error?.config?.retryCount ?? 0
let retryErrorType = null

// First, check for transient network errors
const networkErrorInfo = isTransientNetworkError(error)
if (networkErrorInfo && this.config.retryOnNetworkFailure) {
const networkRetryCount = error.config.networkRetryCount || 0
const networkRetryCount = error?.config?.networkRetryCount || 0
return retryNetworkError(error, networkErrorInfo, networkRetryCount + 1)
}

Expand All @@ -482,7 +503,7 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
var response = error.response
if (!response) {
if (error.code === 'ECONNABORTED') {
const timeoutMs = error.config.timeout || this.config.timeout || 'unknown'
const timeoutMs = error?.config?.timeout || this.config.timeout || 'unknown'
error.response = {
...error.response,
status: 408,
Expand Down
Loading
Loading