Skip to content
Open
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
23 changes: 23 additions & 0 deletions lib/contentstack.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,29 @@ import { getContentstackEndpoint } from '@contentstack/utils'
* const client = contentstack.client({ region: 'eu' })
*
* @prop {string=} params.feature - Feature identifier for user agent header
* @prop {Array<Object>=} params.plugins - Optional array of plugin objects. Each plugin must have `onRequest` and `onResponse` methods.
* @example //Set plugins to intercept and modify requests/responses
* import * as contentstack from '@contentstack/management'
* const client = contentstack.client({
* plugins: [
* {
* onRequest: (request) => {
* // Return modified request
* return {
* ...request,
* headers: {
* ...request.headers,
* 'X-Custom-Header': 'value'
* }
* }
* },
* onResponse: (response) => {
* // Return modified response
* return response
* }
* }
* ]
* })
* @returns {ContentstackClient} Instance of ContentstackClient
*/
export function client (params = {}) {
Expand Down
23 changes: 23 additions & 0 deletions lib/core/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,26 @@ export const validateAndSanitizeConfig = (config) => {
url: config.url.trim() // Sanitize URL by removing whitespace
}
}

/**
* Normalizes and validates plugin array
* @param {Array|undefined} plugins - Array of plugin objects
* @returns {Array} Normalized array of plugins
*/
export function normalizePlugins (plugins) {
if (!plugins) {
return []
}

if (!Array.isArray(plugins)) {
return []
}

return plugins.filter(plugin => {
if (!plugin || typeof plugin !== 'object') {
return false
}
// Plugin must have both onRequest and onResponse methods
return typeof plugin.onRequest === 'function' && typeof plugin.onResponse === 'function'
})
}
119 changes: 118 additions & 1 deletion lib/core/contentstackHTTPClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import axios from 'axios'
import clonedeep from 'lodash/cloneDeep'
import Qs from 'qs'
import { ConcurrencyQueue } from './concurrency-queue'
import { isHost } from './Util'
import { isHost, normalizePlugins } from './Util'

export default function contentstackHttpClient (options) {
const defaultConfig = {
Expand Down Expand Up @@ -109,6 +109,11 @@ export default function contentstackHttpClient (options) {
const instance = axios.create(axiosOptions)
instance.httpClientParams = options
instance.concurrencyQueue = new ConcurrencyQueue({ axios: instance, config })

// Normalize and store plugins
const plugins = normalizePlugins(config.plugins)

// Request interceptor for versioning strategy (must run first)
instance.interceptors.request.use((request) => {
if (request.versioningStrategy && request.versioningStrategy === 'path') {
request.baseURL = request.baseURL.replace('{api-version}', version)
Expand All @@ -117,5 +122,117 @@ export default function contentstackHttpClient (options) {
}
return request
})

// Request interceptor for plugins (runs after versioning)
if (plugins.length > 0) {
instance.interceptors.request.use(
(request) => {
// Run all onRequest hooks sequentially, using return values
let currentRequest = request
for (const plugin of plugins) {
try {
if (typeof plugin.onRequest === 'function') {
const result = plugin.onRequest(currentRequest)
// Use returned value if provided, otherwise use current request
if (result !== undefined) {
currentRequest = result
}
}
} catch (error) {
// Log error and continue with next plugin
if (config.logHandler) {
config.logHandler('error', {
name: 'PluginError',
message: `Error in plugin onRequest: ${error.message}`,
error: error
})
}
}
}
return currentRequest
},
(error) => {
// Handle request errors - run plugins even on error
let currentConfig = error.config
for (const plugin of plugins) {
try {
if (typeof plugin.onRequest === 'function' && currentConfig) {
const result = plugin.onRequest(currentConfig)
// Use returned value if provided, otherwise use current config
if (result !== undefined) {
currentConfig = result
error.config = currentConfig
}
}
} catch (pluginError) {
if (config.logHandler) {
config.logHandler('error', {
name: 'PluginError',
message: `Error in plugin onRequest (error handler): ${pluginError.message}`,
error: pluginError
})
}
}
}
return Promise.reject(error)
}
)

// Response interceptor for plugins
instance.interceptors.response.use(
(response) => {
// Run all onResponse hooks sequentially for successful responses
// Use return values from plugins
let currentResponse = response
for (const plugin of plugins) {
try {
if (typeof plugin.onResponse === 'function') {
const result = plugin.onResponse(currentResponse)
// Use returned value if provided, otherwise use current response
if (result !== undefined) {
currentResponse = result
}
}
} catch (error) {
// Log error and continue with next plugin
if (config.logHandler) {
config.logHandler('error', {
name: 'PluginError',
message: `Error in plugin onResponse: ${error.message}`,
error: error
})
}
}
}
return currentResponse
},
(error) => {
// Handle response errors - run plugins even on error
// Pass the error object (which may contain error.response if server responded)
let currentError = error
for (const plugin of plugins) {
try {
if (typeof plugin.onResponse === 'function') {
const result = plugin.onResponse(currentError)
// Use returned value if provided, otherwise use current error
if (result !== undefined) {
currentError = result
}
}
} catch (pluginError) {
if (config.logHandler) {
config.logHandler('error', {
name: 'PluginError',
message: `Error in plugin onResponse (error handler): ${pluginError.message}`,
error: pluginError
})
}
}
}
return Promise.reject(currentError)
}
)
}

return instance
}
Loading
Loading