Skip to content

Commit

Permalink
fix(sdk-middleware-http): allow to unset content-type (#1751)
Browse files Browse the repository at this point in the history
* feat: allow requests with empty content-type

* fix: adjust file spaces & format

* fix: check null content-type instead of using custom header

* fix: refactor the logic to make it more simple

* fix: solve failing tests and refactor the way of the checking content-type to be more simple

* Create orange-years-rush.md

* Create plenty-wasps-refuse.md

* debugging middleware http

* fix: trying to fix test

* fix: removing console log

* reverting - debugging middleware http

* refactor(sdk-http): allow to unset content-type header

* chore: regenerate lockfile

* chore: reset yarn.lock with master

Co-authored-by: Ajima Chukwuemeka <[email protected]>
Co-authored-by: Chukwuemeka Ajima <[email protected]>
Co-authored-by: danrleyt <[email protected]>
Co-authored-by: Nicola Molinari <[email protected]>
  • Loading branch information
5 people authored Feb 9, 2022
1 parent 7c7a3de commit d392692
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 161 deletions.
7 changes: 7 additions & 0 deletions .changeset/plenty-wasps-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@commercetools/sdk-middleware-http': patch
---

Allow to unset the `content-type` HTTP header by explicitly passing `null` as the value.

A use case for that is using `FormData` as the request body, for example to perform a file upload. The browser generally sets the `content-type` HTTP header automatically.
1 change: 1 addition & 0 deletions integration-tests/cli/discount-codes.it.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('DiscountCode tests', () => {
])

cartDiscount = data[0].body

}, 15000)

afterAll(async () => {
Expand Down
335 changes: 174 additions & 161 deletions packages/sdk-middleware-http/src/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,56 +94,47 @@ export default function createHttpMiddleware({
fetchFunction = fetch
}

return (next: Next): Next => (
request: MiddlewareRequest,
response: MiddlewareResponse
) => {
let abortController: any
const url = host.replace(/\/$/, '') + request.uri
const body =
typeof request.body === 'string' || Buffer.isBuffer(request.body)
? request.body
: // NOTE: `stringify` of `null` gives the String('null')
JSON.stringify(request.body || undefined)
return (next: Next): Next =>
(request: MiddlewareRequest, response: MiddlewareResponse) => {
let abortController: any
const url = host.replace(/\/$/, '') + request.uri
const requestHeader: HttpHeaders = { ...request.headers }

const requestHeader: HttpHeaders = { ...request.headers }
if (!Object.prototype.hasOwnProperty.call(requestHeader, 'Content-Type')) {
requestHeader['Content-Type'] = 'application/json'
}
if (body) {
requestHeader['Content-Length'] = Buffer.byteLength(body).toString()
}
const fetchOptions: RequestOptions = {
method: request.method,
headers: requestHeader,
}
if (credentialsMode) {
fetchOptions.credentials = credentialsMode
}
// If no content-type is provided, defaults to application/json
if (
!Object.prototype.hasOwnProperty.call(requestHeader, 'Content-Type')
) {
requestHeader['Content-Type'] = 'application/json'
}
// Unset the content-type header if explicitly asked to (passing `null` as value).
if (requestHeader['Content-Type'] === null) {
delete requestHeader['Content-Type']
}

if (!retryOnAbort) {
if (timeout || getAbortController || _abortController)
// eslint-disable-next-line
abortController =
(getAbortController ? getAbortController() : null) ||
_abortController ||
new AbortController()
// Ensure body is a string if content type is application/json
const body =
requestHeader['Content-Type'] === 'application/json' &&
typeof request.body !== 'string'
? // NOTE: `stringify` of `null` gives the String('null')
JSON.stringify(request.body || undefined)
: request.body

if (abortController) {
fetchOptions.signal = abortController.signal
if (body && (typeof body === 'string' || Buffer.isBuffer(body))) {
requestHeader['Content-Length'] = Buffer.byteLength(body).toString()
}
}

if (body) {
fetchOptions.body = body
}
let retryCount = 0
// wrap in a fn so we can retry if error occur
function executeFetch() {
if (retryOnAbort) {
const fetchOptions: RequestOptions = {
method: request.method,
headers: requestHeader,
}
if (credentialsMode) {
fetchOptions.credentials = credentialsMode
}

if (!retryOnAbort) {
if (timeout || getAbortController || _abortController)
// eslint-disable-next-line
abortController =
abortController =
(getAbortController ? getAbortController() : null) ||
_abortController ||
new AbortController()
Expand All @@ -152,142 +143,164 @@ export default function createHttpMiddleware({
fetchOptions.signal = abortController.signal
}
}
// Kick off timer for abortController directly before fetch.
let timer
if (timeout)
timer = setTimeout(() => {
abortController.abort()
}, timeout)
fetchFunction(url, fetchOptions)
.then(
(res: Response) => {
if (res.ok) {
if (fetchOptions.method === 'HEAD') {
next(request, {
...response,
statusCode: res.status,

if (body) {
fetchOptions.body = body
}
let retryCount = 0
// wrap in a fn so we can retry if error occur
function executeFetch() {
if (retryOnAbort) {
if (timeout || getAbortController || _abortController)
// eslint-disable-next-line
abortController =
(getAbortController ? getAbortController() : null) ||
_abortController ||
new AbortController()

if (abortController) {
fetchOptions.signal = abortController.signal
}
}
// Kick off timer for abortController directly before fetch.
let timer
if (timeout)
timer = setTimeout(() => {
abortController.abort()
}, timeout)
fetchFunction(url, fetchOptions)
.then(
(res: Response) => {
if (res.ok) {
if (fetchOptions.method === 'HEAD') {
next(request, {
...response,
statusCode: res.status,
})
return
}

res.text().then((result: Object) => {
// Try to parse the response as JSON
let parsed
try {
parsed = result.length > 0 ? JSON.parse(result) : {}
} catch (err) {
if (enableRetry && retryCount < maxRetries) {
setTimeout(
executeFetch,
calcDelayDuration(
retryCount,
retryDelay,
maxRetries,
backoff,
maxDelay
)
)
retryCount += 1
return
}
parsed = result
}

const parsedResponse: Object = {
...response,
body: parsed,
statusCode: res.status,
}

if (includeResponseHeaders)
parsedResponse.headers = parseHeaders(res.headers)

if (includeOriginalRequest) {
parsedResponse.request = {
...fetchOptions,
}
maskAuthData(
parsedResponse.request,
maskSensitiveHeaderData
)
}
next(request, parsedResponse)
})
return
}
if (res.status === 503 && enableRetry)
if (retryCount < maxRetries) {
setTimeout(
executeFetch,
calcDelayDuration(
retryCount,
retryDelay,
maxRetries,
backoff,
maxDelay
)
)
retryCount += 1
return
}

res.text().then((result: Object) => {
// Try to parse the response as JSON
// Server responded with an error. Try to parse it as JSON, then
// return a proper error type with all necessary meta information.
res.text().then((text: any) => {
// Try to parse the error response as JSON
let parsed
try {
parsed = result.length > 0 ? JSON.parse(result) : {}
} catch (err) {
if (enableRetry && retryCount < maxRetries) {
setTimeout(
executeFetch,
calcDelayDuration(
retryCount,
retryDelay,
maxRetries,
backoff,
maxDelay
)
)
retryCount += 1
return
}
parsed = result
parsed = JSON.parse(text)
} catch (error) {
parsed = text
}

const parsedResponse: Object = {
const error: HttpErrorType = createError({
statusCode: res.status,
originalRequest: request,
retryCount,
headers: parseHeaders(res.headers),
...(typeof parsed === 'object'
? { message: parsed.message, body: parsed }
: { message: parsed, body: parsed }),
})
maskAuthData(error.originalRequest, maskSensitiveHeaderData)
// Let the final resolver to reject the promise
const parsedResponse = {
...response,
body: parsed,
error,
statusCode: res.status,
}

if (includeResponseHeaders)
parsedResponse.headers = parseHeaders(res.headers)

if (includeOriginalRequest) {
parsedResponse.request = {
...fetchOptions,
}
maskAuthData(parsedResponse.request, maskSensitiveHeaderData)
}
next(request, parsedResponse)
})
return
}
if (res.status === 503 && enableRetry)
if (retryCount < maxRetries) {
setTimeout(
executeFetch,
calcDelayDuration(
retryCount,
retryDelay,
maxRetries,
backoff,
maxDelay
},
// We know that this is a "network" error thrown by the `fetch` library
(e: Error) => {
if (enableRetry)
if (retryCount < maxRetries) {
setTimeout(
executeFetch,
calcDelayDuration(
retryCount,
retryDelay,
maxRetries,
backoff,
maxDelay
)
)
)
retryCount += 1
return
}

// Server responded with an error. Try to parse it as JSON, then
// return a proper error type with all necessary meta information.
res.text().then((text: any) => {
// Try to parse the error response as JSON
let parsed
try {
parsed = JSON.parse(text)
} catch (error) {
parsed = text
}
retryCount += 1
return
}

const error: HttpErrorType = createError({
statusCode: res.status,
const error = new NetworkError(e.message, {
originalRequest: request,
retryCount,
headers: parseHeaders(res.headers),
...(typeof parsed === 'object'
? { message: parsed.message, body: parsed }
: { message: parsed, body: parsed }),
})
maskAuthData(error.originalRequest, maskSensitiveHeaderData)
// Let the final resolver to reject the promise
const parsedResponse = {
...response,
error,
statusCode: res.status,
}
next(request, parsedResponse)
})
},
// We know that this is a "network" error thrown by the `fetch` library
(e: Error) => {
if (enableRetry)
if (retryCount < maxRetries) {
setTimeout(
executeFetch,
calcDelayDuration(
retryCount,
retryDelay,
maxRetries,
backoff,
maxDelay
)
)
retryCount += 1
return
}

const error = new NetworkError(e.message, {
originalRequest: request,
retryCount,
})
maskAuthData(error.originalRequest, maskSensitiveHeaderData)
next(request, { ...response, error, statusCode: 0 })
}
)
.finally(() => {
clearTimeout(timer)
})
next(request, { ...response, error, statusCode: 0 })
}
)
.finally(() => {
clearTimeout(timer)
})
}
executeFetch()
}
executeFetch()
}
}
Loading

0 comments on commit d392692

Please sign in to comment.