Skip to content

Commit 7ccd784

Browse files
authored
[Feat][DEVX-619]: Add Custom headersWithStringBody to HttpMiddlewareOptions (#1129)
* feat(sdk-client-v3): implement custom header option - add httpMiddleware option to stringify request body on demand - add validation to ensure provided option is correct - add unit tests to validate new functionality and also no regression * Create sharp-files-end.md Add release changeset * chore(sdk-client-v3): implement feedback - rename the option to
1 parent 4694bae commit 7ccd784

File tree

6 files changed

+139
-4
lines changed

6 files changed

+139
-4
lines changed

.changeset/sharp-files-end.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@commercetools/ts-client': minor
3+
---
4+
5+
Add custom `stringBodyContentTypes` to `HttpMiddlewareOptions`
6+
Sometimes we might want to `stringify` a request body before sending it over to
7+
server, this functionality allows the user to add custom `header` entries that
8+
forces the request body to be `stringified` before it's sent over to the backend.

packages/sdk-client-v3/src/middleware/create-http-middleware.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
isBuffer,
2323
maskAuthData,
2424
validateHttpClientOptions,
25+
validateStringBodyHeaderOptions,
2526
} from '../utils'
2627

2728
async function executeRequest({
@@ -132,7 +133,7 @@ async function executeRequest({
132133
export default function createHttpMiddleware(
133134
options: HttpMiddlewareOptions
134135
): Middleware {
135-
// validate response
136+
// validate options
136137
validateHttpClientOptions(options)
137138

138139
const {
@@ -148,13 +149,17 @@ export default function createHttpMiddleware(
148149
includeResponseHeaders = true,
149150
maskSensitiveHeaderData,
150151
httpClientOptions,
152+
stringBodyContentTypes = [],
151153
} = options
152154

153155
return (next: Next) => {
154156
return async (request: MiddlewareRequest): Promise<MiddlewareResponse> => {
155157
const url = host.replace(/\/$/, '') + request.uri
156158
const requestHeader: JsonObject<QueryParam> = { ...request.headers }
157159

160+
// validate custom header
161+
validateStringBodyHeaderOptions(stringBodyContentTypes)
162+
158163
// validate header
159164
if (
160165
!(
@@ -172,9 +177,10 @@ export default function createHttpMiddleware(
172177

173178
// Ensure body is a string if content type is application/{json|graphql}
174179
const body: Record<string, any> | string | Uint8Array =
175-
(constants.HEADERS_CONTENT_TYPES.indexOf(
176-
requestHeader['Content-Type'] as string
177-
) > -1 &&
180+
([
181+
...constants.HEADERS_CONTENT_TYPES,
182+
...stringBodyContentTypes,
183+
].indexOf(requestHeader['Content-Type'] as string) > -1 &&
178184
typeof request.body === 'string') ||
179185
isBuffer(request.body)
180186
? request.body

packages/sdk-client-v3/src/types/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export type HttpMiddlewareOptions = {
218218
enableRetry?: boolean
219219
retryConfig?: RetryOptions
220220
httpClient?: Function
221+
stringBodyContentTypes?: Array<string>
221222
httpClientOptions?: object // will be passed as a second argument to your httpClient function for configuration
222223
getAbortController?: () => AbortController
223224
}

packages/sdk-client-v3/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ export { default as userAgent } from './userAgent'
2020
export {
2121
validate,
2222
// validateUserAgentOptions,
23+
validateStringBodyHeaderOptions,
2324
validateClient, validateHttpClientOptions, validateRetryCodes
2425
} from './validate'

packages/sdk-client-v3/src/utils/validate.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,16 @@ export function validate(
8484
`The "${funcName}" Request object requires a valid method. See https://commercetools.github.io/nodejs/sdk/Glossary.html#clientrequest`
8585
)
8686
}
87+
88+
/**
89+
* @param option
90+
*/
91+
export function validateStringBodyHeaderOptions(
92+
stringBodyContentTypes: string[]
93+
) {
94+
if (!Array.isArray(stringBodyContentTypes)) {
95+
throw new Error(
96+
'`stringBodyContentTypes` option must be an array of strings'
97+
)
98+
}
99+
}

packages/sdk-client-v3/tests/http.test/http-middleware.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,29 @@ describe('Http Middleware.', () => {
9595
)
9696
})
9797

98+
// test('should throw if httpClient is provided but not a function.', async () => {
99+
test('throw when a non-array option is passed as stringBodyContentTypes in the httpMiddlewareOptions', async () => {
100+
const response = createTestResponse({})
101+
102+
const httpMiddlewareOptions = {
103+
host: 'http://api-host.com',
104+
httpClient: jest.fn(() => response),
105+
stringBodyContentTypes: null,
106+
}
107+
108+
try {
109+
const next = () => response
110+
await createHttpMiddleware(httpMiddlewareOptions)(next)(
111+
createTestRequest({})
112+
)
113+
} catch (err) {
114+
expect(err).toBeDefined()
115+
expect(err.message).toMatch(
116+
'`stringBodyContentTypes` option must be an array of strings'
117+
)
118+
}
119+
})
120+
98121
test('should execute a GET request and return a json body.', async () => {
99122
const response = createTestResponse({
100123
body: {},
@@ -469,6 +492,89 @@ describe('Http Middleware.', () => {
469492
}
470493
})
471494

495+
test('should have no effect on a string body.', async () => {
496+
const _response = {
497+
data: { id: 'resource-id-123' },
498+
statusCode: 200,
499+
}
500+
501+
const response = createTestResponse({
502+
text: () => Promise.resolve(JSON.stringify(_response)), // this is not a text response
503+
})
504+
505+
const request = createTestRequest({
506+
uri: '/test-custom-header',
507+
method: 'POST',
508+
body: 'this is a string body',
509+
headers: {
510+
'Content-Type': 'foo',
511+
},
512+
})
513+
514+
const httpMiddlewareOptions: HttpMiddlewareOptions = {
515+
host: 'http://api-host.com',
516+
httpClient: jest.fn(() => response),
517+
stringBodyContentTypes: ['foo'],
518+
}
519+
520+
const next = (req: MiddlewareRequest) => {
521+
expect(req.body).toBeDefined()
522+
expect(typeof req.body).toEqual('string')
523+
return response
524+
}
525+
526+
await createHttpMiddleware(httpMiddlewareOptions)(next)(request)
527+
})
528+
529+
test('should stringify request body if header is included and body is not a string.', async () => {
530+
const _response = {
531+
data: { id: 'resource-id-123' },
532+
statusCode: 200,
533+
}
534+
535+
const response = createTestResponse({
536+
text: () => Promise.resolve(JSON.stringify(_response)),
537+
})
538+
539+
function _fetch(url: string, options: ResponseInit) {
540+
expect(url).toBeDefined()
541+
expect(options).toBeDefined()
542+
return response
543+
}
544+
545+
const request = createTestRequest({
546+
uri: '/test-custom-header',
547+
method: 'POST',
548+
body: { text: 'this is not a string body' },
549+
headers: {
550+
'Content-Type': 'bar',
551+
},
552+
})
553+
554+
const httpMiddlewareOptions: HttpMiddlewareOptions = {
555+
host: 'http://api-host.com',
556+
stringBodyContentTypes: ['bar'],
557+
httpClient: jest
558+
.fn()
559+
.mockImplementation((url: string, fetchOptions: RequestInit) => {
560+
/**
561+
* just before the request is sent over to the
562+
* server the string should already be stringified
563+
*/
564+
expect(fetchOptions.body).toBe('{"text":"this is not a string body"}')
565+
return _fetch(url, fetchOptions)
566+
}),
567+
}
568+
569+
const next = (req: MiddlewareRequest) => {
570+
expect(req.body).toBeDefined()
571+
expect(typeof req.body).toEqual('object')
572+
return response
573+
}
574+
575+
await createHttpMiddleware(httpMiddlewareOptions)(next)(request)
576+
})
577+
472578
describe('::retry test', () => {
473579
test('should throw if `retryCode` is not an array', async () => {
474580
const response = createTestResponse({

0 commit comments

Comments
 (0)