Skip to content
Open
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: 47d7cb6b4cd8701aa289aa2e3975162b1291d4c8a60725d21c103a1ba59df201
checksum: 4a58eb4ee1f54d68387bd005fb76e83a02461441c647d94017743d3442c0f476
- 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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Changelog

## [v1.27.5](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.5) (2026-02-16)
## [v1.27.6](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.5) (2026-02-23)
- Fix
- Skip token refresh on 401 when API returns error_code 161 (environment/permission) so the actual API error is returned instead of triggering refresh and a generic "Unable to refresh token" message
- When token refresh fails after a 401, return the original API error (error_message, error_code) instead of the generic "Unable to refresh token" message

## [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 @@ -446,9 +450,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 @@ -478,13 +485,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 @@ -499,7 +520,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