Skip to content

Commit 3de0215

Browse files
Merge pull request #482 from contentstack/development
staging PR
2 parents 136ee89 + 8b4b4ea commit 3de0215

17 files changed

+587
-38
lines changed

CHANGELOG.md

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

3+
## [v1.27.2](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.2) (2026-01-12)
4+
- Enhancement
5+
- Improved error messages
6+
37
## [v1.27.1](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.1) (2026-01-5)
48
- Fix
59
- Resolve qs dependency version

lib/contentstack.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ import { getContentstackEndpoint } from '@contentstack/utils'
122122
* const client = contentstack.client({ logHandler: (level, data) => {
123123
if (level === 'error' && data) {
124124
const title = [data.name, data.message].filter((a) => a).join(' - ')
125-
console.error(`[error] ${title}`)
125+
console.error(`An error occurred due to ${title}. Review the details and try again.`)
126126
return
127127
}
128128
console.log(`[${level}] ${data}`)
@@ -168,6 +168,29 @@ import { getContentstackEndpoint } from '@contentstack/utils'
168168
* const client = contentstack.client({ region: 'eu' })
169169
*
170170
* @prop {string=} params.feature - Feature identifier for user agent header
171+
* @prop {Array<Object>=} params.plugins - Optional array of plugin objects. Each plugin must have `onRequest` and `onResponse` methods.
172+
* @example //Set plugins to intercept and modify requests/responses
173+
* import * as contentstack from '@contentstack/management'
174+
* const client = contentstack.client({
175+
* plugins: [
176+
* {
177+
* onRequest: (request) => {
178+
* // Return modified request
179+
* return {
180+
* ...request,
181+
* headers: {
182+
* ...request.headers,
183+
* 'X-Custom-Header': 'value'
184+
* }
185+
* }
186+
* },
187+
* onResponse: (response) => {
188+
* // Return modified response
189+
* return response
190+
* }
191+
* }
192+
* ]
193+
* })
171194
* @returns {ContentstackClient} Instance of ContentstackClient
172195
*/
173196
export function client (params = {}) {

lib/contentstackClient.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ export default function contentstackClient ({ http }) {
2626
* const client = contentstack.client()
2727
*
2828
* client.login({ email: <emailid>, password: <password> })
29-
* .then(() => console.log('Logged in successfully'))
29+
* .then(() => console.log('Login successful.'))
3030
*
3131
* @example
3232
* client.login({ email: <emailid>, password: <password>, tfa_token: <tfa_token> })
33-
* .then(() => console.log('Logged in successfully'))
33+
* .then(() => console.log('Login successful.'))
3434
*
3535
* @example
3636
* client.login({ email: <emailid>, password: <password>, mfaSecret: <mfa_secret> })
37-
* .then(() => console.log('Logged in successfully'))
37+
* .then(() => console.log('Login successful.'))
3838
*/
3939
function login (requestBody = {}, params = {}) {
4040
http.defaults.versioningStrategy = 'path'
@@ -210,7 +210,7 @@ export default function contentstackClient ({ http }) {
210210
* const client = contentstack.client()
211211
*
212212
* client.oauth({ appId: <appId>, clientId: <clientId>, redirectUri: <redirectUri>, clientSecret: <clientSecret>, responseType: <responseType>, scope: <scope> })
213-
* .then(() => console.log('Logged in successfully'))
213+
* .then(() => console.log('Login successful.'))
214214
*
215215
*/
216216
function oauth (params = {}) {

lib/core/Util.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { platform, release } from 'os'
2+
import { ERROR_MESSAGES } from './errorMessages'
23

34
const HOST_REGEX = /^(?!(?:(?:https?|ftp):\/\/|internal|localhost|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)))(?:[\w-]+\.contentstack\.(?:io|com)(?::[^\/\s:]+)?|[\w-]+(?:\.[\w-]+)*(?::[^\/\s:]+)?)(?![\/?#])$/ // eslint-disable-line
45

@@ -218,7 +219,7 @@ const isAllowedHost = (hostname) => {
218219

219220
export const validateAndSanitizeConfig = (config) => {
220221
if (!config?.url || typeof config?.url !== 'string') {
221-
throw new Error('Invalid request configuration: missing or invalid URL')
222+
throw new Error(ERROR_MESSAGES.INVALID_URL_CONFIG)
222223
}
223224

224225
// Validate the URL to prevent SSRF attacks
@@ -236,3 +237,26 @@ export const validateAndSanitizeConfig = (config) => {
236237
url: config.url.trim() // Sanitize URL by removing whitespace
237238
}
238239
}
240+
241+
/**
242+
* Normalizes and validates plugin array
243+
* @param {Array|undefined} plugins - Array of plugin objects
244+
* @returns {Array} Normalized array of plugins
245+
*/
246+
export function normalizePlugins (plugins) {
247+
if (!plugins) {
248+
return []
249+
}
250+
251+
if (!Array.isArray(plugins)) {
252+
return []
253+
}
254+
255+
return plugins.filter(plugin => {
256+
if (!plugin || typeof plugin !== 'object') {
257+
return false
258+
}
259+
// Plugin must have both onRequest and onResponse methods
260+
return typeof plugin.onRequest === 'function' && typeof plugin.onResponse === 'function'
261+
})
262+
}

lib/core/concurrency-queue.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Axios from 'axios'
22
import OAuthHandler from './oauthHandler'
33
import { validateAndSanitizeConfig } from './Util'
4+
import { ERROR_MESSAGES } from './errorMessages'
45

56
const defaultConfig = {
67
maxRequests: 5,
@@ -53,23 +54,23 @@ const defaultConfig = {
5354
*/
5455
export function ConcurrencyQueue ({ axios, config }) {
5556
if (!axios) {
56-
throw Error('Axios instance is not present')
57+
throw Error(ERROR_MESSAGES.AXIOS_INSTANCE_MISSING)
5758
}
5859

5960
if (config) {
6061
if (config.maxRequests && config.maxRequests <= 0) {
61-
throw Error('Concurrency Manager Error: minimum concurrent requests is 1')
62+
throw Error(ERROR_MESSAGES.MIN_CONCURRENT_REQUESTS)
6263
} else if (config.retryLimit && config.retryLimit <= 0) {
63-
throw Error('Retry Policy Error: minimum retry limit is 1')
64+
throw Error(ERROR_MESSAGES.MIN_RETRY_LIMIT)
6465
} else if (config.retryDelay && config.retryDelay < 300) {
65-
throw Error('Retry Policy Error: minimum retry delay for requests is 300')
66+
throw Error(ERROR_MESSAGES.MIN_RETRY_DELAY)
6667
}
6768
// Validate network retry configuration
6869
if (config.maxNetworkRetries && config.maxNetworkRetries < 0) {
6970
throw Error('Network Retry Policy Error: maxNetworkRetries cannot be negative')
7071
}
7172
if (config.networkRetryDelay && config.networkRetryDelay < 50) {
72-
throw Error('Network Retry Policy Error: minimum network retry delay is 50ms')
73+
throw Error(ERROR_MESSAGES.MIN_NETWORK_RETRY_DELAY)
7374
}
7475
}
7576

lib/core/contentstackHTTPClient.js

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import axios from 'axios'
22
import clonedeep from 'lodash/cloneDeep'
33
import Qs from 'qs'
44
import { ConcurrencyQueue } from './concurrency-queue'
5-
import { isHost } from './Util'
5+
import { isHost, normalizePlugins } from './Util'
6+
import { ERROR_MESSAGES } from './errorMessages'
67

78
export default function contentstackHttpClient (options) {
89
const defaultConfig = {
@@ -11,7 +12,7 @@ export default function contentstackHttpClient (options) {
1112
logHandler: (level, data) => {
1213
if (level === 'error' && data) {
1314
const title = [data.name, data.message].filter((a) => a).join(' - ')
14-
console.error(`[error] ${title}`)
15+
console.error(ERROR_MESSAGES.ERROR_WITH_TITLE(title))
1516
return
1617
}
1718
console.log(`[${level}] ${data}`)
@@ -109,6 +110,11 @@ export default function contentstackHttpClient (options) {
109110
const instance = axios.create(axiosOptions)
110111
instance.httpClientParams = options
111112
instance.concurrencyQueue = new ConcurrencyQueue({ axios: instance, config })
113+
114+
// Normalize and store plugins
115+
const plugins = normalizePlugins(config.plugins)
116+
117+
// Request interceptor for versioning strategy (must run first)
112118
instance.interceptors.request.use((request) => {
113119
if (request.versioningStrategy && request.versioningStrategy === 'path') {
114120
request.baseURL = request.baseURL.replace('{api-version}', version)
@@ -117,5 +123,117 @@ export default function contentstackHttpClient (options) {
117123
}
118124
return request
119125
})
126+
127+
// Request interceptor for plugins (runs after versioning)
128+
if (plugins.length > 0) {
129+
instance.interceptors.request.use(
130+
(request) => {
131+
// Run all onRequest hooks sequentially, using return values
132+
let currentRequest = request
133+
for (const plugin of plugins) {
134+
try {
135+
if (typeof plugin.onRequest === 'function') {
136+
const result = plugin.onRequest(currentRequest)
137+
// Use returned value if provided, otherwise use current request
138+
if (result !== undefined) {
139+
currentRequest = result
140+
}
141+
}
142+
} catch (error) {
143+
// Log error and continue with next plugin
144+
if (config.logHandler) {
145+
config.logHandler('error', {
146+
name: 'PluginError',
147+
message: `Error in plugin onRequest: ${error.message}`,
148+
error: error
149+
})
150+
}
151+
}
152+
}
153+
return currentRequest
154+
},
155+
(error) => {
156+
// Handle request errors - run plugins even on error
157+
let currentConfig = error.config
158+
for (const plugin of plugins) {
159+
try {
160+
if (typeof plugin.onRequest === 'function' && currentConfig) {
161+
const result = plugin.onRequest(currentConfig)
162+
// Use returned value if provided, otherwise use current config
163+
if (result !== undefined) {
164+
currentConfig = result
165+
error.config = currentConfig
166+
}
167+
}
168+
} catch (pluginError) {
169+
if (config.logHandler) {
170+
config.logHandler('error', {
171+
name: 'PluginError',
172+
message: `Error in plugin onRequest (error handler): ${pluginError.message}`,
173+
error: pluginError
174+
})
175+
}
176+
}
177+
}
178+
return Promise.reject(error)
179+
}
180+
)
181+
182+
// Response interceptor for plugins
183+
instance.interceptors.response.use(
184+
(response) => {
185+
// Run all onResponse hooks sequentially for successful responses
186+
// Use return values from plugins
187+
let currentResponse = response
188+
for (const plugin of plugins) {
189+
try {
190+
if (typeof plugin.onResponse === 'function') {
191+
const result = plugin.onResponse(currentResponse)
192+
// Use returned value if provided, otherwise use current response
193+
if (result !== undefined) {
194+
currentResponse = result
195+
}
196+
}
197+
} catch (error) {
198+
// Log error and continue with next plugin
199+
if (config.logHandler) {
200+
config.logHandler('error', {
201+
name: 'PluginError',
202+
message: `Error in plugin onResponse: ${error.message}`,
203+
error: error
204+
})
205+
}
206+
}
207+
}
208+
return currentResponse
209+
},
210+
(error) => {
211+
// Handle response errors - run plugins even on error
212+
// Pass the error object (which may contain error.response if server responded)
213+
let currentError = error
214+
for (const plugin of plugins) {
215+
try {
216+
if (typeof plugin.onResponse === 'function') {
217+
const result = plugin.onResponse(currentError)
218+
// Use returned value if provided, otherwise use current error
219+
if (result !== undefined) {
220+
currentError = result
221+
}
222+
}
223+
} catch (pluginError) {
224+
if (config.logHandler) {
225+
config.logHandler('error', {
226+
name: 'PluginError',
227+
message: `Error in plugin onResponse (error handler): ${pluginError.message}`,
228+
error: pluginError
229+
})
230+
}
231+
}
232+
}
233+
return Promise.reject(currentError)
234+
}
235+
)
236+
}
237+
120238
return instance
121239
}

lib/core/errorMessages.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Centralized error messages for the Contentstack Management SDK.
3+
* All user-facing error messages should be defined here for consistency and maintainability.
4+
*/
5+
6+
export const ERROR_MESSAGES = {
7+
// Asset errors
8+
ASSET_URL_REQUIRED: 'Asset URL is required. Provide a valid asset URL and try again.',
9+
INVALID_UPLOAD_FORMAT: 'Invalid upload format. Provide a valid file path or Buffer and try again.',
10+
11+
// OAuth errors
12+
OAUTH_BASE_URL_NOT_SET: 'OAuth base URL is not configured. Set the OAuth base URL and try again.',
13+
NO_REFRESH_TOKEN: 'No refresh token available. Authenticate first and try again.',
14+
ACCESS_TOKEN_REQUIRED: 'Access token is required. Provide a valid access token and try again.',
15+
REFRESH_TOKEN_REQUIRED: 'Refresh token is required. Provide a valid refresh token and try again.',
16+
ORGANIZATION_UID_REQUIRED: 'Organization UID is required. Provide a valid organization UID and try again.',
17+
USER_UID_REQUIRED: 'User UID is required. Provide a valid user UID and try again.',
18+
TOKEN_EXPIRY_REQUIRED: 'Token expiry time is required. Provide a valid expiry time and try again.',
19+
AUTH_CODE_NOT_FOUND: 'Authorization code not found in redirect URL. Verify the redirect URL and try again.',
20+
NO_USER_AUTHORIZATIONS: 'No authorizations found for the current user. Verify user permissions and try again.',
21+
NO_APP_AUTHORIZATIONS: 'No authorizations found for the app. Verify app configuration and try again.',
22+
23+
// Concurrency queue errors
24+
AXIOS_INSTANCE_MISSING: 'HTTP client is required. Initialize the HTTP client and try again.',
25+
MIN_CONCURRENT_REQUESTS: 'Concurrency Manager Error: Minimum concurrent requests must be at least 1.',
26+
MIN_RETRY_LIMIT: 'Retry Policy Error: Minimum retry limit must be at least 1.',
27+
MIN_RETRY_DELAY: 'Retry Policy Error: Minimum retry delay must be at least 300ms.',
28+
MIN_NETWORK_RETRY_DELAY: 'Network Retry Policy Error: Minimum network retry delay must be at least 50ms.',
29+
30+
// Request configuration errors
31+
INVALID_URL_CONFIG: 'Invalid request configuration: URL is missing or invalid. Provide a valid URL and try again.',
32+
33+
// General errors
34+
ERROR_WITH_TITLE: (title) => `An error occurred due to ${title}. Review the details and try again.`,
35+
36+
// Content type errors
37+
PARAMETER_NAME_REQUIRED: 'Parameter name is required. Provide a valid parameter name and try again.'
38+
}
39+
40+
export default ERROR_MESSAGES

0 commit comments

Comments
 (0)