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
3 changes: 3 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ fileignoreconfig:
checksum: 4043efd843e24da9afd0272c55ef4b0432e3374b2ca12b913f1a6654df3f62be
- filename: test/unit/contentstack-test.js
checksum: 2597efae3c1ab8cc173d5bf205f1c76932211f8e0eb2a16444e055d83481976c
- filename: test/unit/concurrency-Queue-test.js
checksum: 186438f9eb9ba4e7fd7f335dbea2afbae9ae969b7ae3ab1b517ec7a1633d255e
version: "1.0"




4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [v1.27.3](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.3) (2026-01-21)
- Fix
- 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

## [v1.27.2](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.2) (2026-01-12)
- Enhancement
- Improved error messages
Expand Down
6 changes: 6 additions & 0 deletions lib/core/concurrency-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,12 @@ export function ConcurrencyQueue ({ axios, config }) {
return Promise.reject(responseHandler(error))
}
} 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
const apiErrorCode = response.data?.error_code
if (apiErrorCode === 294) {
return Promise.reject(error)
}

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

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/management",
"version": "1.27.2",
"version": "1.27.3",
"description": "The Content Management API is used to manage the content of your Contentstack account",
"main": "./dist/node/contentstack-management.js",
"browser": "./dist/web/contentstack-management.js",
Expand Down
109 changes: 109 additions & 0 deletions test/unit/concurrency-Queue-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,115 @@ describe('Concurrency queue test', () => {
})
.catch(done)
})

it('should not refresh token when error_code is 294 (2FA required) with 401 status', done => {
let refreshTokenCallCount = 0
const refreshTokenStub = sinon.stub().callsFake(() => {
refreshTokenCallCount++
return Promise.resolve({ authorization: 'Bearer new_token' })
})

const axiosWithRefresh = client({
baseURL: `${host}:${port}`,
authorization: 'Bearer <token_value>',
refreshToken: refreshTokenStub
})

const mock = new MockAdapter(axiosWithRefresh.axiosInstance)
mock.onGet('/test2fa').reply(401, {
error_message: 'Please login using the Two-Factor verification Token',
error_code: 294,
errors: [],
statusCode: 401,
tfa_type: 'totp_authenticator'
})

axiosWithRefresh.axiosInstance.get('/test2fa')
.then(() => {
done(new Error('Expected error was not thrown'))
})
.catch((error) => {
// Verify refreshToken was NOT called
expect(refreshTokenCallCount).to.equal(0)
expect(refreshTokenStub.called).to.equal(false)

// Verify the raw error response has error_code 294
expect(error.response.status).to.equal(401)
expect(error.response.data.error_code).to.equal(294)
expect(error.response.data.error_message).to.include('Two-Factor verification')
expect(error.response.data.tfa_type).to.equal('totp_authenticator')
done()
})
.catch(done)
})

it('should refresh token when 401 status without error_code 294', done => {
const axios2 = client({
baseURL: `${host}:${port}`
})
const axios = client({
baseURL: `${host}:${port}`,
authorization: 'Bearer <token_value>',
refreshToken: () => {
return new Promise((resolve, reject) => {
return axios2.login().then((res) => {
resolve({ authorization: res.token })
}).catch((error) => {
reject(error)
})
})
}
})

// First request will fail with 401, trigger refresh, then succeed
axios.axiosInstance.get('/unauthorized')
.then((response) => {
// Should succeed after token refresh
expect(response.data.randomInteger).to.equal(123)
done()
})
.catch(done)
})

it('should preserve error_code 294 when present with 401 status', done => {
const refreshTokenStub = sinon.stub()
refreshTokenStub.returns(Promise.resolve({ authorization: 'Bearer new_token' }))

const axiosWithRefresh = client({
baseURL: `${host}:${port}`,
authorization: 'Bearer <token_value>',
refreshToken: refreshTokenStub
})

const mock = new MockAdapter(axiosWithRefresh.axiosInstance)
mock.onGet('/test2fa294').reply(401, {
error_message: 'Please login using the Two-Factor verification Token',
error_code: 294,
errors: [],
statusCode: 401,
tfa_type: 'totp_authenticator'
})

axiosWithRefresh.axiosInstance.get('/test2fa294')
.then(() => {
done(new Error('Expected error was not thrown'))
})
.catch((error) => {
// Verify refreshToken was NOT called
expect(refreshTokenStub.called).to.equal(false)

// Verify the raw error response preserves error_code 294
expect(error.response.status).to.equal(401)
expect(error.response.data.error_code).to.equal(294)
expect(error.response.data.error_message).to.include('Two-Factor verification')
expect(error.response.data.tfa_type).to.equal('totp_authenticator')

// The key test: error_code 294 should be preserved in response.data
// This ensures our fix in concurrency-queue.js is working (no token refresh attempted)
done()
})
.catch(done)
})
})

function makeConcurrencyQueue (config) {
Expand Down
Loading