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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,6 @@ tsconfig.json
.dccache
dist
jsdocs
.early.coverage
.early.coverage
# Snyk Security Extension - AI Rules (auto-generated)
.cursor/rules/snyk_rules.mdc
2 changes: 1 addition & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fileignoreconfig:
ignore_detectors:
- filecontent
- filename: package-lock.json
checksum: 92b88ce00603ede68344bac6bd6bf76bdb76f1e5f5ba8d1d0c79da2b72c5ecc0
checksum: 4a58eb4ee1f54d68387bd005fb76e83a02461441c647d94017743d3442c0f476
- filename: test/unit/ContentstackClient-test.js
checksum: 5d8519b5b93c715e911a62b4033614cc4fb3596eabf31c7216ecb4cc08604a73
- filename: .husky/pre-commit
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [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
Expand Down
43 changes: 32 additions & 11 deletions lib/core/concurrency-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,16 +404,33 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
this.config.authtoken = token.authtoken
}
}).catch((error) => {
const apiError = this._last401ApiError
if (apiError) {
this._last401ApiError = null
}
this.queue.forEach(queueItem => {
queueItem.reject({
errorCode: '401',
errorMessage: (error instanceof Error) ? error.message : error,
code: 'Unauthorized',
message: 'Unable to refresh token',
name: 'Token Error',
config: queueItem.request,
stack: (error instanceof Error) ? error.stack : null
})
if (apiError) {
queueItem.reject({
errorCode: apiError.error_code ?? '401',
errorMessage: apiError.error_message || apiError.message || ((error instanceof Error) ? error.message : String(error)),
code: 'Unauthorized',
message: apiError.error_message || apiError.message || 'Unable to refresh token',
name: 'Token Error',
config: queueItem.request,
stack: (error instanceof Error) ? error.stack : null,
response: { status: 401, statusText: 'Unauthorized', data: apiError }
})
} else {
queueItem.reject({
errorCode: '401',
errorMessage: (error instanceof Error) ? error.message : error,
code: 'Unauthorized',
message: 'Unable to refresh token',
name: 'Token Error',
config: queueItem.request,
stack: (error instanceof Error) ? error.stack : null
})
}
})
this.queue = []
this.running = []
Expand Down Expand Up @@ -515,9 +532,11 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
return Promise.reject(responseHandler(err))
}
} else if ((response.status === 401 && this.config.refreshToken)) {
// If error_code is 294 (2FA required), don't retry/refresh - pass through the error as-is
// Retry/refresh only for authentication-related 401s (e.g. token expiry). Do not retry
// when the API returns a specific error_code for non-auth issues (2FA, permission, etc.).
const apiErrorCode = response.data?.error_code
if (apiErrorCode === 294) {
const NON_AUTH_401_ERROR_CODES = new Set([294, 161]) // 294 = 2FA required, 161 = env/permission
if (apiErrorCode !== undefined && apiErrorCode !== null && NON_AUTH_401_ERROR_CODES.has(apiErrorCode)) {
const err = runPluginOnResponseForError(error)
return Promise.reject(responseHandler(err))
}
Expand All @@ -530,6 +549,8 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
return Promise.reject(responseHandler(err))
}
this.running.shift()
// Store original API error so we can return it if refresh fails (instead of generic message)
this._last401ApiError = response.data
// Cool down the running requests
delay(wait, response.status === 401)
error.config.retryCount = networkError
Expand Down
Loading
Loading