Skip to content

Commit 4f852bd

Browse files
Merge pull request #510 from contentstack/staging
DX | 23-02-2026 | Release
2 parents 865d3c7 + c5d3602 commit 4f852bd

File tree

7 files changed

+275
-158
lines changed

7 files changed

+275
-158
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,6 @@ tsconfig.json
6868
.dccache
6969
dist
7070
jsdocs
71-
.early.coverage
71+
.early.coverage
72+
# Snyk Security Extension - AI Rules (auto-generated)
73+
.cursor/rules/snyk_rules.mdc

.talismanrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ fileignoreconfig:
1111
ignore_detectors:
1212
- filecontent
1313
- filename: package-lock.json
14-
checksum: 92b88ce00603ede68344bac6bd6bf76bdb76f1e5f5ba8d1d0c79da2b72c5ecc0
14+
checksum: 4a58eb4ee1f54d68387bd005fb76e83a02461441c647d94017743d3442c0f476
1515
- filename: test/unit/ContentstackClient-test.js
1616
checksum: 5d8519b5b93c715e911a62b4033614cc4fb3596eabf31c7216ecb4cc08604a73
1717
- filename: .husky/pre-commit

CHANGELOG.md

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

3+
## [v1.27.6](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.5) (2026-02-23)
4+
- Fix
5+
- 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
6+
- 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
7+
38
## [v1.27.5](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.5) (2026-02-11)
49
- Fix
510
- 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

lib/core/concurrency-queue.js

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -404,16 +404,33 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
404404
this.config.authtoken = token.authtoken
405405
}
406406
}).catch((error) => {
407+
const apiError = this._last401ApiError
408+
if (apiError) {
409+
this._last401ApiError = null
410+
}
407411
this.queue.forEach(queueItem => {
408-
queueItem.reject({
409-
errorCode: '401',
410-
errorMessage: (error instanceof Error) ? error.message : error,
411-
code: 'Unauthorized',
412-
message: 'Unable to refresh token',
413-
name: 'Token Error',
414-
config: queueItem.request,
415-
stack: (error instanceof Error) ? error.stack : null
416-
})
412+
if (apiError) {
413+
queueItem.reject({
414+
errorCode: apiError.error_code ?? '401',
415+
errorMessage: apiError.error_message || apiError.message || ((error instanceof Error) ? error.message : String(error)),
416+
code: 'Unauthorized',
417+
message: apiError.error_message || apiError.message || 'Unable to refresh token',
418+
name: 'Token Error',
419+
config: queueItem.request,
420+
stack: (error instanceof Error) ? error.stack : null,
421+
response: { status: 401, statusText: 'Unauthorized', data: apiError }
422+
})
423+
} else {
424+
queueItem.reject({
425+
errorCode: '401',
426+
errorMessage: (error instanceof Error) ? error.message : error,
427+
code: 'Unauthorized',
428+
message: 'Unable to refresh token',
429+
name: 'Token Error',
430+
config: queueItem.request,
431+
stack: (error instanceof Error) ? error.stack : null
432+
})
433+
}
417434
})
418435
this.queue = []
419436
this.running = []
@@ -515,9 +532,11 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
515532
return Promise.reject(responseHandler(err))
516533
}
517534
} else if ((response.status === 401 && this.config.refreshToken)) {
518-
// If error_code is 294 (2FA required), don't retry/refresh - pass through the error as-is
535+
// Retry/refresh only for authentication-related 401s (e.g. token expiry). Do not retry
536+
// when the API returns a specific error_code for non-auth issues (2FA, permission, etc.).
519537
const apiErrorCode = response.data?.error_code
520-
if (apiErrorCode === 294) {
538+
const NON_AUTH_401_ERROR_CODES = new Set([294, 161]) // 294 = 2FA required, 161 = env/permission
539+
if (apiErrorCode !== undefined && apiErrorCode !== null && NON_AUTH_401_ERROR_CODES.has(apiErrorCode)) {
521540
const err = runPluginOnResponseForError(error)
522541
return Promise.reject(responseHandler(err))
523542
}
@@ -530,6 +549,8 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
530549
return Promise.reject(responseHandler(err))
531550
}
532551
this.running.shift()
552+
// Store original API error so we can return it if refresh fails (instead of generic message)
553+
this._last401ApiError = response.data
533554
// Cool down the running requests
534555
delay(wait, response.status === 401)
535556
error.config.retryCount = networkError

0 commit comments

Comments
 (0)