Skip to content

Commit 3a00e71

Browse files
Merge pull request #495 from contentstack/staging
DX | 02-02-2026 | Release
2 parents 4acf4a7 + 8a55979 commit 3a00e71

File tree

9 files changed

+508
-385
lines changed

9 files changed

+508
-385
lines changed

.talismanrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ fileignoreconfig:
99
ignore_detectors:
1010
- filecontent
1111
- filename: package-lock.json
12-
checksum: 17b5bbabcc58beaa180a7fa931fc3fb407ee0e3447d47da224f60118c0a4c294
12+
checksum: 751efa34d2f832c7b99771568b5125d929dab095784b6e4ea659daaa612994c8
1313
- filename: .husky/pre-commit
1414
checksum: 52a664f536cf5d1be0bea19cb6031ca6e8107b45b6314fe7d47b7fad7d800632
1515
- filename: test/sanity-check/api/user-test.js

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.4](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.4) (2026-02-02)
4+
- Fix
5+
- Removed content-type header from the release delete method
6+
- Plugin `onResponse` hook is now called for error responses; errors handled by the concurrency queue (e.g. 5xx, retries exhausted) were previously not running plugin hooks
7+
38
## [v1.27.3](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.3) (2026-01-21)
49
- Fix
510
- Skip token refresh and preserve error_code 294 when 2FA is required (error_code 294 with 401 status) to prevent error code conversion from 294 to 401

lib/core/concurrency-queue.js

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const defaultConfig = {
5252
* @returns {Object} ConcurrencyQueue instance with request/response interceptors attached to Axios.
5353
* @throws {Error} If axios instance is not provided or configuration is invalid.
5454
*/
55-
export function ConcurrencyQueue ({ axios, config }) {
55+
export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
5656
if (!axios) {
5757
throw Error(ERROR_MESSAGES.AXIOS_INSTANCE_MISSING)
5858
}
@@ -436,6 +436,30 @@ export function ConcurrencyQueue ({ axios, config }) {
436436
return response
437437
}
438438

439+
// Run plugin onResponse hooks for errors so plugins see every error (including
440+
// those that are rejected without going through the plugin response interceptor).
441+
const runPluginOnResponseForError = (error) => {
442+
if (!plugins || !plugins.length) return error
443+
let currentError = error
444+
for (const plugin of plugins) {
445+
try {
446+
if (typeof plugin.onResponse === 'function') {
447+
const result = plugin.onResponse(currentError)
448+
if (result !== undefined) currentError = result
449+
}
450+
} catch (e) {
451+
if (this.config.logHandler) {
452+
this.config.logHandler('error', {
453+
name: 'PluginError',
454+
message: `Error in plugin onResponse (error handler): ${e.message}`,
455+
error: e
456+
})
457+
}
458+
}
459+
}
460+
return currentError
461+
}
462+
439463
const responseErrorHandler = error => {
440464
let networkError = error.config.retryCount
441465
let retryErrorType = null
@@ -449,7 +473,8 @@ export function ConcurrencyQueue ({ axios, config }) {
449473

450474
// Original retry logic for non-network errors
451475
if (!this.config.retryOnError || networkError > this.config.retryLimit) {
452-
return Promise.reject(responseHandler(error))
476+
const err = runPluginOnResponseForError(error)
477+
return Promise.reject(responseHandler(err))
453478
}
454479

455480
// Check rate limit remaining header before retrying
@@ -465,20 +490,23 @@ export function ConcurrencyQueue ({ axios, config }) {
465490
}
466491
response = error.response
467492
} else {
468-
return Promise.reject(responseHandler(error))
493+
const err = runPluginOnResponseForError(error)
494+
return Promise.reject(responseHandler(err))
469495
}
470496
} else if ((response.status === 401 && this.config.refreshToken)) {
471497
// If error_code is 294 (2FA required), don't retry/refresh - pass through the error as-is
472498
const apiErrorCode = response.data?.error_code
473499
if (apiErrorCode === 294) {
474-
return Promise.reject(error)
500+
const err = runPluginOnResponseForError(error)
501+
return Promise.reject(responseHandler(err))
475502
}
476503

477504
retryErrorType = `Error with status: ${response.status}`
478505
networkError++
479506

480507
if (networkError > this.config.retryLimit) {
481-
return Promise.reject(responseHandler(error))
508+
const err = runPluginOnResponseForError(error)
509+
return Promise.reject(responseHandler(err))
482510
}
483511
this.running.shift()
484512
// Cool down the running requests
@@ -492,19 +520,22 @@ export function ConcurrencyQueue ({ axios, config }) {
492520
networkError++
493521
return this.retry(error, retryErrorType, networkError, wait)
494522
}
495-
return Promise.reject(responseHandler(error))
523+
const err = runPluginOnResponseForError(error)
524+
return Promise.reject(responseHandler(err))
496525
}
497526

498527
this.retry = (error, retryErrorType, retryCount, waittime) => {
499528
let delaytime = waittime
500529
if (retryCount > this.config.retryLimit) {
501-
return Promise.reject(responseHandler(error))
530+
const err = runPluginOnResponseForError(error)
531+
return Promise.reject(responseHandler(err))
502532
}
503533
if (this.config.retryDelayOptions) {
504534
if (this.config.retryDelayOptions.customBackoff) {
505535
delaytime = this.config.retryDelayOptions.customBackoff(retryCount, error)
506536
if (delaytime && delaytime <= 0) {
507-
return Promise.reject(responseHandler(error))
537+
const err = runPluginOnResponseForError(error)
538+
return Promise.reject(responseHandler(err))
508539
}
509540
} else if (this.config.retryDelayOptions.base) {
510541
delaytime = this.config.retryDelayOptions.base * retryCount

lib/core/contentstackHTTPClient.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,11 @@ export default function contentstackHttpClient (options) {
109109
}
110110
const instance = axios.create(axiosOptions)
111111
instance.httpClientParams = options
112-
instance.concurrencyQueue = new ConcurrencyQueue({ axios: instance, config })
113112

114-
// Normalize and store plugins
115-
const plugins = normalizePlugins(config.plugins)
113+
// Normalize and store plugins before ConcurrencyQueue so plugin interceptors
114+
// run after the queue's (plugin sees responses/errors before they reach the queue).
115+
// Use options.plugins so hooks run against the same plugin references (spies work in tests).
116+
const plugins = normalizePlugins(options.plugins || config.plugins)
116117

117118
// Request interceptor for versioning strategy (must run first)
118119
instance.interceptors.request.use((request) => {
@@ -235,5 +236,7 @@ export default function contentstackHttpClient (options) {
235236
)
236237
}
237238

239+
instance.concurrencyQueue = new ConcurrencyQueue({ axios: instance, config, plugins })
240+
238241
return instance
239242
}

lib/stack/release/index.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
create,
44
fetch,
55
update,
6-
deleteEntity,
76
query
87
} from '../../entity'
98
import error from '../../core/contentstackError'
@@ -94,7 +93,31 @@ export function Release (http, data = {}) {
9493
* client.stack({ api_key: 'api_key'}).release('release_uid').delete()
9594
* .then((response) => console.log(response.notice))
9695
*/
97-
this.delete = deleteEntity(http)
96+
this.delete = (function (http) {
97+
return async function (param = {}) {
98+
try {
99+
const requestConfig = {
100+
headers: { ...cloneDeep(this.stackHeaders) },
101+
params: { ...cloneDeep(param) }
102+
}
103+
// Omit Content-Type for DELETE; no body is sent and fastify v5 rejects it when present
104+
requestConfig.headers['Content-Type'] = null
105+
const response = await http.delete(this.urlPath, requestConfig)
106+
if (response.data) {
107+
return response.data
108+
}
109+
if (response.status >= 200 && response.status < 300) {
110+
return {
111+
status: response.status,
112+
statusText: response.statusText
113+
}
114+
}
115+
throw error(response)
116+
} catch (err) {
117+
throw error(err)
118+
}
119+
}
120+
})(http)
98121

99122
/**
100123
* @description A ReleaseItem is a set of entries and assets that needs to be deployed (published or unpublished) all at once to a particular environment.

0 commit comments

Comments
 (0)