Skip to content

Commit 844a049

Browse files
committed
feat: add plugin support for request/response interception in Contentstack HTTP client
1 parent b9b5c73 commit 844a049

File tree

5 files changed

+477
-3
lines changed

5 files changed

+477
-3
lines changed

lib/contentstack.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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/core/Util.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,26 @@ export const validateAndSanitizeConfig = (config) => {
236236
url: config.url.trim() // Sanitize URL by removing whitespace
237237
}
238238
}
239+
240+
/**
241+
* Normalizes and validates plugin array
242+
* @param {Array|undefined} plugins - Array of plugin objects
243+
* @returns {Array} Normalized array of plugins
244+
*/
245+
export function normalizePlugins (plugins) {
246+
if (!plugins) {
247+
return []
248+
}
249+
250+
if (!Array.isArray(plugins)) {
251+
return []
252+
}
253+
254+
return plugins.filter(plugin => {
255+
if (!plugin || typeof plugin !== 'object') {
256+
return false
257+
}
258+
// Plugin must have both onRequest and onResponse methods
259+
return typeof plugin.onRequest === 'function' && typeof plugin.onResponse === 'function'
260+
})
261+
}

lib/core/contentstackHTTPClient.js

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ 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'
66

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

0 commit comments

Comments
 (0)