Skip to content

Commit

Permalink
Merge branch 'master' into gen-sdk-updates
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] authored Jun 14, 2024
2 parents b99710e + fd482b7 commit ae64c57
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { ClientBuilder as ClientBuilderV3 } from '@commercetools/ts-client/src'
import {
authMiddlewareOptions,
authMiddlewareOptionsV3,
httpMiddlewareOptionsV3,
projectKey,
} from '../test-utils'
import { createApiBuilderFromCtpClient } from '../../../src'
import fetch from 'node-fetch'

describe('testing error cases', () => {
it('should throw error when a product type is not found', async () => {
try {
const ctpClientV3 = new ClientBuilderV3()
.withHttpMiddleware(httpMiddlewareOptionsV3)
.withConcurrentModificationMiddleware()
.withClientCredentialsFlow(authMiddlewareOptions)
.withClientCredentialsFlow(authMiddlewareOptionsV3)
.build()

const apiRootV3 = createApiBuilderFromCtpClient(
Expand All @@ -32,4 +33,34 @@ describe('testing error cases', () => {
expect(e.statusCode).toEqual(404)
}
})

it('should retry when aborted', async () => {
let isFirstCall = true

const client = new ClientBuilderV3()
.withClientCredentialsFlow(authMiddlewareOptionsV3)
.withHttpMiddleware({
...httpMiddlewareOptionsV3,
enableRetry: true,
retryConfig: {
retryOnAbort: true,
},
httpClient: function (url, args) {
if (isFirstCall) {
isFirstCall = false
return fetch(url, { ...args, signal: AbortSignal.timeout(10) })
} else {
return fetch(url, args)
}
},
})
.build()

const apiRootV3 = createApiBuilderFromCtpClient(client).withProjectKey({
projectKey,
})
const response = await apiRootV3.get().execute()

expect(response.statusCode).toBe(200)
})
})
3 changes: 2 additions & 1 deletion packages/sdk-client-v3/src/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export type HttpMiddlewareOptions = {
retryConfig?: RetryOptions
httpClient: Function
getAbortController?: () => AbortController
httpClientOptions?: object
httpClientOptions?: object // will be passed as a second argument to your httpClient function for configuration
}

export type RetryOptions = RetryMiddlewareOptions
Expand All @@ -242,6 +242,7 @@ export type RetryMiddlewareOptions = {
maxRetries?: number
retryDelay?: number
maxDelay?: typeof Infinity
retryOnAbort?: boolean
retryCodes?: Array<number | string>
}

Expand Down
51 changes: 40 additions & 11 deletions packages/sdk-client-v3/src/utils/executor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { HttpClientConfig, IResponse, TResponse } from '../types/types'
import { calculateRetryDelay, sleep, validateRetryCodes } from '../utils'

function predicate(retryCodes: Array<string | number>, response: any) {
return !(
function hasResponseRetryCode(
retryCodes: Array<string | number>,
response: any
) {
return (
// retryCodes.includes(response?.error?.message) ||
[503, ...retryCodes].includes(response?.status || response?.statusCode)
)
Expand Down Expand Up @@ -33,13 +36,15 @@ export default async function executor(request: HttpClientConfig) {

const data: TResponse = await executeHttpClientRequest(
async (options: HttpClientConfig): Promise<TResponse> => {
const { enableRetry, retryConfig } = rest
const { enableRetry, retryConfig, abortController } = rest
const {
retryCodes = [],
maxDelay = Infinity,
maxRetries = 3,
backoff = true,
retryDelay = 200,
// If set to true reinitialize the abort controller when the timeout is reached and apply the retry config
retryOnAbort = true,
} = retryConfig || {}

let result: string,
Expand Down Expand Up @@ -68,17 +73,41 @@ export default async function executor(request: HttpClientConfig) {
}

async function executeWithRetry<T = any>(): Promise<T> {
// first attempt
let _response = await execute()

if (predicate(retryCodes, _response)) return _response
const executeWithTryCatch = async (retryCodes, retryWhenAborted) => {
let _response = {} as any
try {
_response = await execute()
if (
_response.status > 299 &&
hasResponseRetryCode(retryCodes, _response)
)
return { _response, shouldRetry: true }
} catch (e) {
if (e.name.includes('AbortError') && retryWhenAborted) {
return { _response: e, shouldRetry: true }
} else {
throw e
}
}
return { _response, shouldRetry: false }
}

const retryWhenAborted =
retryOnAbort || !abortController || !abortController.signal
// first attempt
let { _response, shouldRetry } = await executeWithTryCatch(
retryCodes,
retryWhenAborted
)
// retry attempts
while (enableRetry && retryCount < maxRetries) {
while (enableRetry && shouldRetry && retryCount < maxRetries) {
retryCount++
_response = await execute()

if (predicate(retryCodes, _response)) return _response
const execution = await executeWithTryCatch(
retryCodes,
retryWhenAborted
)
_response = execution._response
shouldRetry = execution.shouldRetry

// delay next execution
const timer = calculateRetryDelay({
Expand Down
5 changes: 0 additions & 5 deletions packages/sdk-client-v3/src/utils/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ 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.timeout && !options.getAbortController)
throw new Error(
'`AbortController` is not available. Please pass in `getAbortController` as an option or have AbortController globally available when using timeout.'
)
}

/**
Expand Down
189 changes: 170 additions & 19 deletions packages/sdk-client-v3/tests/http.test/http-middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HttpMiddlewareOptions, MiddlewareRequest } from '../../src'
import { Buffer } from 'buffer/'
import { createHttpMiddleware } from '../../src/middleware'
import fetch from 'node-fetch'

function createTestRequest(options) {
return {
Expand Down Expand Up @@ -52,23 +53,6 @@ describe('Http Middleware.', () => {
).toThrow()
})

test('should throw if timeout is provided and `AbortController` is not.', () => {
const response = createTestResponse({})
const httpMiddlewareOptions = {
host: 'http://api-host.com',
httpClient: jest.fn(),
timeout: 2000,
getAbortController: null,
}

const next = () => response
expect(() =>
createHttpMiddleware(httpMiddlewareOptions)(next)(createTestRequest({}))
).toThrow(
/`AbortController` is not available. Please pass in `getAbortController` as an option or have AbortController globally available when using timeout./
)
})

test('should throw if the set timeout value elapses', async () => {
const response = createTestResponse({ statusCode: 0 })
const httpMiddlewareOptions: HttpMiddlewareOptions = {
Expand Down Expand Up @@ -474,7 +458,7 @@ describe('Http Middleware.', () => {
test('should retry request based on response status code', async () => {
const response = createTestResponse({
data: {},
statusCode: 504,
status: 504,
})

const request = createTestRequest({
Expand Down Expand Up @@ -515,7 +499,7 @@ describe('Http Middleware.', () => {
test('should retry request with exponential backoff', async () => {
const response = createTestResponse({
data: {},
statusCode: 503,
status: 503,
})

const request = createTestRequest({
Expand Down Expand Up @@ -552,5 +536,172 @@ describe('Http Middleware.', () => {

await createHttpMiddleware(httpMiddlewareOptions)(next)(request)
})

test('should not retry request by default when aborted', async () => {
const request = createTestRequest({
uri: '/delay/1',
method: 'GET',
})

const next = (req: MiddlewareRequest) => {
return createTestResponse(req.response)
}

const httpMiddlewareOptions: HttpMiddlewareOptions = {
host: 'https://httpbin.org',
httpClient: fetch,
timeout: 500,
enableRetry: true,
retryConfig: {
maxRetries: 2,
retryOnAbort: false,
},
}

const response = await createHttpMiddleware(httpMiddlewareOptions)(next)(
request
)

expect(response.error.message).toContain('aborted')
})

test('should throw an error when httpClient is invalid', async () => {
const request = createTestRequest({
uri: '/test',
method: 'GET',
})

const testResponse = createTestResponse({
data: {},
statusCode: 503,
})

const next = (req: MiddlewareRequest) => {
return testResponse
}
const testError = new Error()

const httpMiddlewareOptions: HttpMiddlewareOptions = {
host: 'https://httpbin.org',
httpClient: jest.fn(() => {
throw testError
}),
timeout: 500,
enableRetry: true,
retryConfig: {
maxRetries: 2,
retryOnAbort: false,
},
}

try {
await createHttpMiddleware(httpMiddlewareOptions)(next)(request)
} catch (e) {
expect(e).toEqual(testError)
}
})

test('should retry request on aborted when retryOnAbort=true', async () => {
const testData = 'this is a string response data'

const request = createTestRequest({
uri: '/delay/1',
method: 'GET',
})

const next = (req: MiddlewareRequest) => {
return createTestResponse(req.response)
}

let httpClientCalled = false

const httpMiddlewareOptions: HttpMiddlewareOptions = {
host: 'https://httpbin.org',
httpClient: (url, options) => {
if (httpClientCalled) {
return createTestResponse({
data: testData,
statusCode: 200,
headers: {
'server-time': '05:07',
},
})
} else {
httpClientCalled = true
return fetch(url, options)
}
},
timeout: 500,
enableRetry: true,
retryConfig: {
maxRetries: 2,
retryDelay: 500,
retryOnAbort: true,
},
}

const response = await createHttpMiddleware(httpMiddlewareOptions)(next)(
request
)

expect(response.error).toBeUndefined()
expect(response.statusCode).toEqual(200)
expect(response.body).toEqual(testData)
})

test('should retry CTP request when retry=true and retryOnAbort=false', async () => {
const testData = 'this is a string response data'
const testResponse = createTestResponse({
data: {},
statusCode: 504,
})

const request = createTestRequest({
uri: '/error-url/retry',
method: 'POST',
body: { id: 'test-id' },
headers: {
'Content-Type': 'image/jpeg',
},
})

let httpClientCalled = false

const httpMiddlewareOptions: HttpMiddlewareOptions = {
host: 'https://httpbin.org',
httpClient: (url, options) => {
if (httpClientCalled) {
return createTestResponse({
data: testData,
statusCode: 200,
headers: {
'server-time': '05:07',
},
})
} else {
httpClientCalled = true
return testResponse
}
},
enableRetry: true,
retryConfig: {
maxRetries: 3,
backoff: false,
retryDelay: 200,
retryCodes: [504],
retryOnAbort: false,
},
}

const next = (req: MiddlewareRequest) => {
return testResponse
}

const response = await createHttpMiddleware(httpMiddlewareOptions)(next)(
request
)

expect(response).toBeDefined()
})
})
})

0 comments on commit ae64c57

Please sign in to comment.