Skip to content

Commit

Permalink
[Feat][DEVX-487] Add httpClient configuration options (#848)
Browse files Browse the repository at this point in the history
* feat(sdk-client-v3): add http client config option

- add config object to auth and http middleware options
- add object validation to ensure option is truly an object
- add unit and integration tests
- minor refactor

* chore(release-changeset): add release changeset

- add release changeset for new SDK release version
- add axios as development dependencies (mainly for running tests)

* chore(ci-error): fix ci error

- properly sort root dependencies list

* chore(feedback): implement feedback

- implement feedback

* chore(feedback): implement feedback

- implement feedback
  • Loading branch information
ajimae authored Nov 15, 2024
1 parent 02bb40f commit 39c5298
Show file tree
Hide file tree
Showing 17 changed files with 241 additions and 19 deletions.
6 changes: 6 additions & 0 deletions .changeset/serious-insects-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@commercetools/ts-client': patch
---

- add `httpClientOptions` to all supported `authMiddlewareOption` and `httpMiddlewareOptions`
- add options used for configuring the behaviour of the supported `httpClients` (fetch and axios)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@preconstruct/cli": "2.8.8",
"@types/jest": "29.5.13",
"@types/node": "^20.0.0",
"axios": "^1.7.7",
"babel-jest": "29.7.0",
"buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import {
MiddlewareRequest,
Next,
ClientBuilder,
type HttpMiddlewareOptions,
} from '@commercetools/ts-client/src'
import { createApiBuilderFromCtpClient } from '../../../src'
import { randomUUID } from 'crypto'
import axios from 'axios'

import * as matchers from 'jest-extended'
expect.extend(matchers)
Expand Down Expand Up @@ -187,6 +189,90 @@ describe('Concurrent Modification Middleware', () => {
})
})

describe('Http clients and http client options', () => {
it('Axios should throw error internally and cut off subsequent execution', async () => {
let isCalled = false

const after = () => {
return (next: Next) => {
return (request: MiddlewareRequest) => {
isCalled = true
return next(request)
}
}
}

const http: HttpMiddlewareOptions = {
...httpMiddlewareOptionsV3,
httpClient: axios,
host: 'https://commercetools.com', // should fail (404 incorrect host)
httpClientOptions: {
validateStatus: () => false, // axios default
},
}

const v3Client = new ClientBuilderV3()
.withClientCredentialsFlow(authMiddlewareOptionsV3)
.withHttpMiddleware(http)
// should not be called (since axios will throw internal error)
.withAfterExecutionMiddleware({ middleware: after })
.build()

const api = createApiBuilderFromCtpClient(v3Client).withProjectKey({
projectKey,
})

await api
.get()
.execute()
.catch(() => null)
expect(isCalled).toBe(false)
})

it('Axios Should not throw error internally, continue executions', async () => {
let isCalled = false

const after = () => {
return (next: Next) => {
return (request: MiddlewareRequest) => {
isCalled = true
return next(request)
}
}
}

const auth = {
...authMiddlewareOptionsV3,
}

const http: HttpMiddlewareOptions = {
...httpMiddlewareOptionsV3,
httpClient: axios,
host: 'https://commercetools.com', // should fail (404 incorrect host)
httpClientOptions: {
validateStatus: () => true, // change axios default
},
}

const v3Client = new ClientBuilderV3()
.withClientCredentialsFlow(auth)
.withHttpMiddleware(http)
// should be called (since axios won't throw internal error)
.withAfterExecutionMiddleware({ middleware: after })
.build()

const api = createApiBuilderFromCtpClient(v3Client).withProjectKey({
projectKey,
})

await api
.get()
.execute()
.catch(() => null)
expect(isCalled).toBe(true)
})
})

describe('Before and after execution middlewares', () => {
it('should execute before execution middleware before after execution middleware', async () => {
let beforeRequest
Expand Down
2 changes: 1 addition & 1 deletion packages/platform-sdk/test/integration-tests/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const authMiddlewareOptions = {
},
tokenCache,
scopes: [`manage_project:${projectKey}`],
fetch,
httpClient: fetch,
}

export const authMiddlewareOptionsV3 = {
Expand Down
1 change: 1 addition & 0 deletions packages/sdk-client-v3/src/client/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export default class ClientBuilder {
host: oauthUri,
projectKey: projectKey || this.projectKey,
credentials,
httpClient: httpClient || fetch,
scopes,
}).withHttpMiddleware({
host: baseUri,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default function createAuthMiddlewareForAnonymousSessionFlow(
tokenCache,
tokenCacheKey,
httpClient: options.httpClient || fetch,
httpClientOptions: options.httpClientOptions,
...buildRequestForAnonymousSessionFlow(options),
userOption: options,
next,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import {
import { buildRequestForRefreshTokenFlow } from './auth-request-builder'

export async function executeRequest(options: executeRequestOptions) {
const { httpClient, tokenCache, userOption, tokenCacheObject } = options
const {
httpClient,
httpClientOptions,
tokenCache,
userOption,
tokenCacheObject,
} = options

let url = options.url
let body = options.body
Expand Down Expand Up @@ -62,6 +68,7 @@ export async function executeRequest(options: executeRequestOptions) {
'Content-Length': byteLength(body),
},
httpClient,
httpClientOptions,
body,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default function createAuthMiddlewareForClientCredentialsFlow(
tokenCacheKey,
tokenCacheObject,
httpClient: options.httpClient || fetch,
httpClientOptions: options.httpClientOptions,
...buildRequestForClientCredentialsFlow(options),
next,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default function createAuthMiddlewareForPasswordFlow(
tokenCache,
tokenCacheKey,
httpClient: options.httpClient || fetch,
httpClientOptions: options.httpClientOptions,
...buildRequestForPasswordFlow(options),
userOption: options,
next,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default function createAuthMiddlewareForRefreshTokenFlow(
request,
tokenCache,
httpClient: options.httpClient || fetch,
httpClientOptions: options.httpClientOptions,
...buildRequestForRefreshTokenFlow(options),
next,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
getHeaders,
isBuffer,
maskAuthData,
validateHttpOptions,
validateHttpClientOptions,
} from '../utils'

async function executeRequest({
Expand Down Expand Up @@ -142,7 +142,7 @@ export default function createHttpMiddleware(
options: HttpMiddlewareOptions
): Middleware {
// validate response
validateHttpOptions(options)
validateHttpClientOptions(options)

const {
host,
Expand Down
21 changes: 12 additions & 9 deletions packages/sdk-client-v3/src/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ export type AuthMiddlewareOptions = {
scopes?: Array<string>
// For internal usage only
oauthUri?: string
httpClient?: Function
tokenCache?: TokenCache
httpClient: Function
httpClientOptions?: object
}

export type TokenCacheOptions = {
Expand Down Expand Up @@ -138,7 +139,8 @@ export type RefreshAuthMiddlewareOptions = {
tokenCache?: TokenCache,
// For internal usage only
oauthUri?: string
httpClient?: Function
httpClient: Function
httpClientOptions?: object
}

/* Request */
Expand All @@ -147,9 +149,10 @@ type requestBaseOptions = {
body: string
basicAuth: string
request: MiddlewareRequest
tokenCache: TokenCache,
tokenCacheKey?: TokenCacheOptions,
tokenCache: TokenCache
tokenCacheKey?: TokenCacheOptions
tokenCacheObject?: TokenStore
httpClientOptions?: object
}

export type executeRequestOptions = requestBaseOptions & {
Expand All @@ -160,11 +163,9 @@ export type executeRequestOptions = requestBaseOptions & {

export type AuthMiddlewareBaseOptions = requestBaseOptions & {
request: MiddlewareRequest
httpClient?: Function
httpClient: Function
}

export type RequestState = boolean

export type Task = {
request: MiddlewareRequest
next?: Next
Expand All @@ -187,7 +188,8 @@ export type PasswordAuthMiddlewareOptions = {
tokenCache?: TokenCache,
// For internal usage only
oauthUri?: string
httpClient?: Function
httpClient: Function
httpClientOptions?: object
}

export type TokenInfo = {
Expand All @@ -214,8 +216,8 @@ export type HttpMiddlewareOptions = {
enableRetry?: boolean
retryConfig?: RetryOptions
httpClient: Function
getAbortController?: () => AbortController
httpClientOptions?: object // will be passed as a second argument to your httpClient function for configuration
getAbortController?: () => AbortController
}

export type RetryOptions = RetryMiddlewareOptions
Expand Down Expand Up @@ -297,6 +299,7 @@ export type IClientOptions = {
enableRetry?: boolean
retryConfig?: RetryOptions
maskSensitiveHeaderData?: boolean
httpClientOptions?: object
}

export type HttpClientOptions = IClientOptions & Optional
Expand Down
11 changes: 8 additions & 3 deletions packages/sdk-client-v3/src/utils/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,10 @@ export default async function executor(request: HttpClientConfig) {

async function execute() {
return httpClient(url, {
...rest,
...options,
...rest,
headers: {
...rest.headers,
...options.headers,

// axios header encoding
'Accept-Encoding': 'application/json',
Expand Down Expand Up @@ -156,7 +155,13 @@ export default async function executor(request: HttpClientConfig) {
* middleware options or from
* http client config
*/
{}

/**
* we want to suppress axios internal
* error handling behaviour to make it
* consistent with native fetch.
*/
{ validateStatus: (status: number) => true }
)

return data
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-client-v3/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export { default as userAgent } from './userAgent'
export {
validate,
// validateUserAgentOptions,
validateClient, validateHttpOptions, validateRetryCodes
validateClient, validateHttpClientOptions, validateRetryCodes
} from './validate'
10 changes: 9 additions & 1 deletion packages/sdk-client-v3/src/utils/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
* validate some essential http options
* @param options
*/
export function validateHttpOptions(options: HttpMiddlewareOptions) {
export function validateHttpClientOptions(options: HttpMiddlewareOptions) {
if (!options.host)
throw new Error(
'Request `host` or `url` is missing or invalid, please pass in a valid host e.g `host: http://a-valid-host-url`'
Expand All @@ -19,6 +19,14 @@ export function validateHttpOptions(options: HttpMiddlewareOptions) {
throw new Error(
'An `httpClient` is not available, please pass in a `fetch` or `axios` instance as an option or have them globally available.'
)

if (
options.httpClientOptions &&
Object.prototype.toString.call(options.httpClientOptions) !==
'[object Object]'
) {
throw new Error('`httpClientOptions` must be an object type')
}
}

/**
Expand Down
Loading

0 comments on commit 39c5298

Please sign in to comment.