diff --git a/CHANGELOG.md b/CHANGELOG.md index a36857f18..9b52e4968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# [5.0.0](https://github.com/uploadcare/uploadcare-js-api-clients/compare/v4.3.1...v5.0.0) (2022-09-13) + + +* refactor(upload-client)!: drop `multipartMaxAttempts` option ([178436e](https://github.com/uploadcare/uploadcare-js-api-clients/commit/178436e83c956a2b428e11bc4d76af27b627bcdb)) + + +### Features + +* **api-client-utils:** add `UploadcareNetworkError` ([0e917d2](https://github.com/uploadcare/uploadcare-js-api-clients/commit/0e917d29cfeb151c2484d89d5b69ba9eafaf31e9)) +* **upload-client:** add `retryNetworkErrorMaxTimes` option to specify retries count on network errors ([400fedd](https://github.com/uploadcare/uploadcare-js-api-clients/commit/400fedd0e4a143ca4a18cf79d7b1385e797396a8)) +* **upload-client:** throw `UploadcareNetworkError` instead of `Error` ([f7e3f55](https://github.com/uploadcare/uploadcare-js-api-clients/commit/f7e3f55626e047e214bce0d18e4ff34fc7445509)) + + +### BREAKING CHANGES + +* option `multipartMaxAttempts` is dropped. Use `retryNetworkErrorMaxTimes` instead. It will affect all the requests, not only multipart uploads. + + + ## [4.3.1](https://github.com/uploadcare/uploadcare-js-api-clients/compare/v4.3.0...v4.3.1) (2022-08-24) diff --git a/package-lock.json b/package-lock.json index 4bd7d4ce9..550c84572 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@uploadcare/api-clients", - "version": "4.3.1", + "version": "5.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@uploadcare/api-clients", - "version": "4.3.1", + "version": "5.0.0", "license": "MIT", "workspaces": [ "packages/api-client-utils", @@ -11413,12 +11413,12 @@ }, "packages/api-client-utils": { "name": "@uploadcare/api-client-utils", - "version": "4.3.1", + "version": "5.0.0", "license": "MIT" }, "packages/rest-client": { "name": "@uploadcare/rest-client", - "version": "4.3.1", + "version": "5.0.0", "license": "MIT", "dependencies": { "blueimp-md5": "^2.19.0", @@ -11426,7 +11426,7 @@ }, "devDependencies": { "@types/blueimp-md5": "^2.18.0", - "@uploadcare/upload-client": "^4.3.1", + "@uploadcare/upload-client": "^5.0.0", "ts-node": "^10.8.1" } }, @@ -11457,7 +11457,7 @@ }, "packages/upload-client": { "name": "@uploadcare/upload-client", - "version": "4.3.1", + "version": "5.0.0", "license": "MIT", "dependencies": { "form-data": "^4.0.0", @@ -11469,7 +11469,7 @@ "@types/express-serve-static-core": "^4.17.28", "@types/koa": "2.13.4", "@types/ws": "8.5.3", - "@uploadcare/api-client-utils": "^4.3.1", + "@uploadcare/api-client-utils": "^5.0.0", "chalk": "^4.1.2", "data-uri-to-buffer": "3.0.1", "dataurl-to-blob": "0.0.1", @@ -13187,7 +13187,7 @@ "version": "file:packages/rest-client", "requires": { "@types/blueimp-md5": "^2.18.0", - "@uploadcare/upload-client": "^4.3.1", + "@uploadcare/upload-client": "^5.0.0", "blueimp-md5": "^2.19.0", "node-fetch": "^3.2.6", "ts-node": "^10.8.1" @@ -13218,7 +13218,7 @@ "@types/express-serve-static-core": "^4.17.28", "@types/koa": "2.13.4", "@types/ws": "8.5.3", - "@uploadcare/api-client-utils": "^4.3.1", + "@uploadcare/api-client-utils": "^5.0.0", "chalk": "^4.1.2", "data-uri-to-buffer": "3.0.1", "dataurl-to-blob": "0.0.1", diff --git a/package.json b/package.json index 2c07a9f7f..bd4be85ff 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@uploadcare/api-clients", "private": true, "type": "module", - "version": "4.3.1", + "version": "5.0.0", "license": "MIT", "workspaces": [ "packages/api-client-utils", diff --git a/packages/api-client-utils/package.json b/packages/api-client-utils/package.json index a6a4a5109..8a058d79c 100644 --- a/packages/api-client-utils/package.json +++ b/packages/api-client-utils/package.json @@ -1,6 +1,6 @@ { "name": "@uploadcare/api-client-utils", - "version": "4.3.1", + "version": "5.0.0", "type": "module", "main": "dist/index.browser.js", "module": "dist/index.browser.js", diff --git a/packages/api-client-utils/src/UploadcareNetworkError.test.ts b/packages/api-client-utils/src/UploadcareNetworkError.test.ts new file mode 100644 index 000000000..6e343ff38 --- /dev/null +++ b/packages/api-client-utils/src/UploadcareNetworkError.test.ts @@ -0,0 +1,12 @@ +import { UploadcareNetworkError } from './UploadcareNetworkError' + +describe('UploadcareNetworkError', () => { + it('should work', () => { + const progressEvent = new Event('ProgressEvent') as ProgressEvent + const error = new UploadcareNetworkError(progressEvent) + expect(error.name).toBe('UploadcareNetworkError') + expect(error.message).toBe('Network error') + expect(error instanceof UploadcareNetworkError).toBeTruthy() + expect(error.originalProgressEvent).toBe(progressEvent) + }) +}) diff --git a/packages/api-client-utils/src/UploadcareNetworkError.ts b/packages/api-client-utils/src/UploadcareNetworkError.ts new file mode 100644 index 000000000..1ab3c3099 --- /dev/null +++ b/packages/api-client-utils/src/UploadcareNetworkError.ts @@ -0,0 +1,13 @@ +export class UploadcareNetworkError extends Error { + originalProgressEvent: ProgressEvent + + constructor(progressEvent: ProgressEvent) { + super() + + this.name = 'UploadcareNetworkError' + this.message = 'Network error' + Object.setPrototypeOf(this, UploadcareNetworkError.prototype) + + this.originalProgressEvent = progressEvent + } +} diff --git a/packages/api-client-utils/src/index.ts b/packages/api-client-utils/src/index.ts index 8285b700d..239a2bb3b 100644 --- a/packages/api-client-utils/src/index.ts +++ b/packages/api-client-utils/src/index.ts @@ -10,6 +10,7 @@ export { export { isNode } from './isNode' export { isObject } from './isObject' export { retrier } from './retrier' +export { UploadcareNetworkError } from './UploadcareNetworkError' export { ContentInfo } from './types/ContentInfo' export { ImageInfo } from './types/ImageInfo' export { Metadata } from './types/Metadata' diff --git a/packages/api-client-utils/src/version.ts b/packages/api-client-utils/src/version.ts index bfbcd7dc1..50f03b40d 100644 --- a/packages/api-client-utils/src/version.ts +++ b/packages/api-client-utils/src/version.ts @@ -1 +1 @@ -export default '4.3.1' +export default '5.0.0' diff --git a/packages/rest-client/package.json b/packages/rest-client/package.json index a51559f03..8a439eef8 100644 --- a/packages/rest-client/package.json +++ b/packages/rest-client/package.json @@ -1,6 +1,6 @@ { "name": "@uploadcare/rest-client", - "version": "4.3.1", + "version": "5.0.0", "description": "Library for work with Uploadcare Rest API", "type": "module", "module": "./dist/index.node.js", @@ -37,7 +37,7 @@ "keywords": ["uploadcare", "file", "rest", "api"], "devDependencies": { "@types/blueimp-md5": "^2.18.0", - "@uploadcare/upload-client": "^4.3.1", + "@uploadcare/upload-client": "^5.0.0", "ts-node": "^10.8.1" }, "dependencies": { diff --git a/packages/rest-client/src/tools/retryIfThrottled.ts b/packages/rest-client/src/tools/retryIfThrottled.ts index 0468840db..ca76ca873 100644 --- a/packages/rest-client/src/tools/retryIfThrottled.ts +++ b/packages/rest-client/src/tools/retryIfThrottled.ts @@ -14,6 +14,7 @@ function getTimeoutFromThrottledRequest(response: Response): number { return retryAfterSeconds * 1000 } +// TODO: implement NetworkError retry export function retryIfThrottled( fn: () => Promise, retryThrottledMaxTimes: number diff --git a/packages/rest-client/src/version.ts b/packages/rest-client/src/version.ts index bfbcd7dc1..50f03b40d 100644 --- a/packages/rest-client/src/version.ts +++ b/packages/rest-client/src/version.ts @@ -1 +1 @@ -export default '4.3.1' +export default '5.0.0' diff --git a/packages/upload-client/README.md b/packages/upload-client/README.md index 7050e8044..dcfee5610 100644 --- a/packages/upload-client/README.md +++ b/packages/upload-client/README.md @@ -364,6 +364,19 @@ Sets the maximum number of attempts to retry throttled requests. Defaults to `1`. +#### `retryNetworkErrorMaxTimes: number` + +Sets the maximum number of attempts to retry requests that failed with a network error. + +Defaults to `3`. + +The delay between attempts equals attempt number, i.e. + +- first attempt - 1 second delay +- second attempt - 2 seconds delay +- third attempt - 3 seconds delay +- ... + #### `multipartChunkSize: number` This option is only applicable when handling local files. @@ -411,7 +424,7 @@ Metadata is additional, arbitrary data, associated with uploaded file. Non-string values will be converted to `string`. `undefined` values will be ignored. -See [REST API reference][uc-docs-metadata] for details. +See [docs][uc-file-metadata] and [REST API][uc-docs-metadata] for details. ## Testing @@ -476,3 +489,4 @@ request at [hello@uploadcare.com][uc-email-hello]. [build-url]: https://github.com/uploadcare/uploadcare-js-api-clients/actions/workflows/checks.yml [uc-docs-upload-api]: https://uploadcare.com/docs/api_reference/upload/?utm_source=github&utm_campaign=uploadcare-js-api-clients [uc-docs-metadata]: https://uploadcare.com/api-refs/rest-api/v0.7.0/#tag/File-Metadata +[uc-file-metadata]: https://uploadcare.com/docs/file-metadata/ diff --git a/packages/upload-client/package.json b/packages/upload-client/package.json index d7572f033..5a1974371 100644 --- a/packages/upload-client/package.json +++ b/packages/upload-client/package.json @@ -1,6 +1,6 @@ { "name": "@uploadcare/upload-client", - "version": "4.3.1", + "version": "5.0.0", "description": "Library for work with Uploadcare Upload API", "type": "module", "module": "./dist/index.node.js", @@ -65,7 +65,7 @@ "koa-body": "5.0.0", "mock-socket": "9.0.3", "start-server-and-test": "1.14.0", - "@uploadcare/api-client-utils": "^4.3.1", + "@uploadcare/api-client-utils": "^5.0.0", "chalk": "^4.1.2" }, "dependencies": { diff --git a/packages/upload-client/src/api/base.ts b/packages/upload-client/src/api/base.ts index 3faed08cf..8e3745c3b 100644 --- a/packages/upload-client/src/api/base.ts +++ b/packages/upload-client/src/api/base.ts @@ -5,7 +5,7 @@ import { defaultSettings, defaultFilename } from '../defaultSettings' import { getUserAgent } from '../tools/getUserAgent' import { camelizeKeys, CustomUserAgent } from '@uploadcare/api-client-utils' import { UploadClientError } from '../tools/errors' -import retryIfThrottled from '../tools/retryIfThrottled' +import { retryIfFailed } from '../tools/retryIfFailed' /* Types */ import { Uuid, ProgressCallback, Metadata } from './types' @@ -36,6 +36,7 @@ export type BaseOptions = { userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number metadata?: Metadata } @@ -59,10 +60,11 @@ export default function base( integration, userAgent, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes, metadata }: BaseOptions ): Promise { - return retryIfThrottled( + return retryIfFailed( () => request({ method: 'POST', @@ -101,6 +103,6 @@ export default function base( return response } }), - retryThrottledRequestMaxTimes + { retryNetworkErrorMaxTimes, retryThrottledRequestMaxTimes } ) } diff --git a/packages/upload-client/src/api/fromUrl.ts b/packages/upload-client/src/api/fromUrl.ts index 142f0af92..f33b92dc1 100644 --- a/packages/upload-client/src/api/fromUrl.ts +++ b/packages/upload-client/src/api/fromUrl.ts @@ -8,7 +8,7 @@ import getUrl from '../tools/getUrl' import defaultSettings from '../defaultSettings' import { getUserAgent } from '../tools/getUserAgent' import { UploadClientError } from '../tools/errors' -import retryIfThrottled from '../tools/retryIfThrottled' +import { retryIfFailed } from '../tools/retryIfFailed' import { getStoreValue } from '../tools/getStoreValue' export enum TypeEnum { @@ -67,6 +67,7 @@ export type FromUrlOptions = { userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number metadata?: Metadata } @@ -89,10 +90,11 @@ export default function fromUrl( integration, userAgent, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes, metadata }: FromUrlOptions ): Promise { - return retryIfThrottled( + return retryIfFailed( () => request({ method: 'POST', @@ -128,6 +130,6 @@ export default function fromUrl( return response } }), - retryThrottledRequestMaxTimes + { retryNetworkErrorMaxTimes, retryThrottledRequestMaxTimes } ) } diff --git a/packages/upload-client/src/api/fromUrlStatus.ts b/packages/upload-client/src/api/fromUrlStatus.ts index fd6ba28c4..77b934682 100644 --- a/packages/upload-client/src/api/fromUrlStatus.ts +++ b/packages/upload-client/src/api/fromUrlStatus.ts @@ -8,7 +8,7 @@ import getUrl from '../tools/getUrl' import defaultSettings from '../defaultSettings' import { getUserAgent } from '../tools/getUserAgent' import { UploadClientError } from '../tools/errors' -import retryIfThrottled from '../tools/retryIfThrottled' +import { retryIfFailed } from '../tools/retryIfFailed' export enum Status { Unknown = 'unknown', @@ -69,6 +69,7 @@ export type FromUrlStatusOptions = { userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number } /** @@ -82,10 +83,11 @@ export default function fromUrlStatus( signal, integration, userAgent, - retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes + retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes }: FromUrlStatusOptions = {} ): Promise { - return retryIfThrottled( + return retryIfFailed( () => request({ method: 'GET', @@ -118,6 +120,6 @@ export default function fromUrlStatus( return response } }), - retryThrottledRequestMaxTimes + { retryNetworkErrorMaxTimes, retryThrottledRequestMaxTimes } ) } diff --git a/packages/upload-client/src/api/group.ts b/packages/upload-client/src/api/group.ts index 6b8810236..76ff4aa52 100644 --- a/packages/upload-client/src/api/group.ts +++ b/packages/upload-client/src/api/group.ts @@ -8,7 +8,7 @@ import getUrl from '../tools/getUrl' import defaultSettings from '../defaultSettings' import { getUserAgent } from '../tools/getUserAgent' import { UploadClientError } from '../tools/errors' -import retryIfThrottled from '../tools/retryIfThrottled' +import { retryIfFailed } from '../tools/retryIfFailed' export type GroupOptions = { publicKey: string @@ -25,6 +25,7 @@ export type GroupOptions = { userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number } type Response = GroupInfo | FailedResponse @@ -44,10 +45,11 @@ export default function group( source, integration, userAgent, - retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes + retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes }: GroupOptions ): Promise { - return retryIfThrottled( + return retryIfFailed( () => request({ method: 'POST', @@ -79,6 +81,6 @@ export default function group( return response } }), - retryThrottledRequestMaxTimes + { retryNetworkErrorMaxTimes, retryThrottledRequestMaxTimes } ) } diff --git a/packages/upload-client/src/api/groupInfo.ts b/packages/upload-client/src/api/groupInfo.ts index 6464b860e..e04a68938 100644 --- a/packages/upload-client/src/api/groupInfo.ts +++ b/packages/upload-client/src/api/groupInfo.ts @@ -8,7 +8,7 @@ import getUrl from '../tools/getUrl' import defaultSettings from '../defaultSettings' import { getUserAgent } from '../tools/getUserAgent' import { UploadClientError } from '../tools/errors' -import retryIfThrottled from '../tools/retryIfThrottled' +import { retryIfFailed } from '../tools/retryIfFailed' export type GroupInfoOptions = { publicKey: string @@ -21,6 +21,7 @@ export type GroupInfoOptions = { userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number } type Response = GroupInfo | FailedResponse @@ -37,10 +38,11 @@ export default function groupInfo( source, integration, userAgent, - retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes + retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes }: GroupInfoOptions ): Promise { - return retryIfThrottled( + return retryIfFailed( () => request({ method: 'GET', @@ -69,6 +71,6 @@ export default function groupInfo( return response } }), - retryThrottledRequestMaxTimes + { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes } ) } diff --git a/packages/upload-client/src/api/info.ts b/packages/upload-client/src/api/info.ts index 9f16ec6cd..ca378ba8a 100644 --- a/packages/upload-client/src/api/info.ts +++ b/packages/upload-client/src/api/info.ts @@ -4,7 +4,7 @@ import defaultSettings from '../defaultSettings' import { getUserAgent } from '../tools/getUserAgent' import { camelizeKeys, CustomUserAgent } from '@uploadcare/api-client-utils' import { UploadClientError } from '../tools/errors' -import retryIfThrottled from '../tools/retryIfThrottled' +import { retryIfFailed } from '../tools/retryIfFailed' /* Types */ import { Uuid, FileInfo } from './types' @@ -24,6 +24,7 @@ export type InfoOptions = { userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number } /** @@ -38,10 +39,11 @@ export default function info( source, integration, userAgent, - retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes + retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes }: InfoOptions ): Promise { - return retryIfThrottled( + return retryIfFailed( () => request({ method: 'GET', @@ -70,6 +72,6 @@ export default function info( return response } }), - retryThrottledRequestMaxTimes + { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes } ) } diff --git a/packages/upload-client/src/api/multipartComplete.ts b/packages/upload-client/src/api/multipartComplete.ts index aaf83babf..a87dce2ad 100644 --- a/packages/upload-client/src/api/multipartComplete.ts +++ b/packages/upload-client/src/api/multipartComplete.ts @@ -7,7 +7,7 @@ import buildFormData from '../tools/buildFormData' import getUrl from '../tools/getUrl' import defaultSettings from '../defaultSettings' import { getUserAgent } from '../tools/getUserAgent' -import retryIfThrottled from '../tools/retryIfThrottled' +import { retryIfFailed } from '../tools/retryIfFailed' import { UploadClientError } from '../tools/errors' export type MultipartCompleteOptions = { @@ -18,6 +18,7 @@ export type MultipartCompleteOptions = { integration?: string userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number } type Response = FailedResponse | FileInfo @@ -34,10 +35,11 @@ export default function multipartComplete( signal, integration, userAgent, - retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes + retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes }: MultipartCompleteOptions ): Promise { - return retryIfThrottled( + return retryIfFailed( () => request({ method: 'POST', @@ -66,6 +68,6 @@ export default function multipartComplete( return response } }), - retryThrottledRequestMaxTimes + { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes } ) } diff --git a/packages/upload-client/src/api/multipartStart.ts b/packages/upload-client/src/api/multipartStart.ts index 26a3488db..06f9e7fbf 100644 --- a/packages/upload-client/src/api/multipartStart.ts +++ b/packages/upload-client/src/api/multipartStart.ts @@ -11,7 +11,7 @@ import { defaultContentType } from '../defaultSettings' import { getUserAgent } from '../tools/getUserAgent' -import retryIfThrottled from '../tools/retryIfThrottled' +import { retryIfFailed } from '../tools/retryIfFailed' import { UploadClientError } from '../tools/errors' import { getStoreValue } from '../tools/getStoreValue' @@ -29,6 +29,7 @@ export type MultipartStartOptions = { integration?: string userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number metadata?: Metadata } @@ -60,10 +61,12 @@ export default function multipartStart( integration, userAgent, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes, + metadata }: MultipartStartOptions ): Promise { - return retryIfThrottled( + return retryIfFailed( () => request({ method: 'POST', @@ -104,6 +107,6 @@ export default function multipartStart( return response } }), - retryThrottledRequestMaxTimes + { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes } ) } diff --git a/packages/upload-client/src/api/multipartUpload.ts b/packages/upload-client/src/api/multipartUpload.ts index 5307bcd6d..0f23844b3 100644 --- a/packages/upload-client/src/api/multipartUpload.ts +++ b/packages/upload-client/src/api/multipartUpload.ts @@ -4,6 +4,8 @@ import request from '../request/request.node' import { ComputableProgressInfo, ProgressCallback } from './types' import { NodeFile, BrowserFile } from '../request/types' +import { retryIfFailed } from '../tools/retryIfFailed' +import defaultSettings from '../defaultSettings' export type MultipartUploadOptions = { publicKey?: string @@ -11,6 +13,7 @@ export type MultipartUploadOptions = { onProgress?: ProgressCallback integration?: string retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number } export type MultipartUploadResponse = { @@ -24,25 +27,37 @@ export type MultipartUploadResponse = { export default function multipartUpload( part: NodeFile | BrowserFile, url: MultipartPart, - { signal, onProgress }: MultipartUploadOptions + { + signal, + onProgress, + retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes + }: MultipartUploadOptions ): Promise { - return request({ - method: 'PUT', - url, - data: part, - // Upload request can't be non-computable because we always know exact size - onProgress: onProgress as ProgressCallback, - signal - }) - .then((result) => { - // hack for node ¯\_(ツ)_/¯ - if (onProgress) - onProgress({ - isComputable: true, - value: 1 - }) + return retryIfFailed( + () => + request({ + method: 'PUT', + url, + data: part, + // Upload request can't be non-computable because we always know exact size + onProgress: onProgress as ProgressCallback, + signal + }) + .then((result) => { + // hack for node ¯\_(ツ)_/¯ + if (onProgress) + onProgress({ + isComputable: true, + value: 1 + }) - return result - }) - .then(({ status }) => ({ code: status })) + return result + }) + .then(({ status }) => ({ code: status })), + { + retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes + } + ) } diff --git a/packages/upload-client/src/defaultSettings.ts b/packages/upload-client/src/defaultSettings.ts index f5208106a..4040aaf98 100644 --- a/packages/upload-client/src/defaultSettings.ts +++ b/packages/upload-client/src/defaultSettings.ts @@ -9,11 +9,11 @@ const defaultSettings: DefaultSettings = { baseURL: 'https://upload.uploadcare.com', maxContentLength: 50 * 1024 * 1024, // 50 MB retryThrottledRequestMaxTimes: 1, + retryNetworkErrorMaxTimes: 3, multipartMinFileSize: 25 * 1024 * 1024, // 25 MB multipartChunkSize: 5 * 1024 * 1024, // 5 MB multipartMinLastPartSize: 1024 * 1024, // 1MB maxConcurrentRequests: 4, - multipartMaxAttempts: 3, pollingTimeoutMilliseconds: 10000, pusherKey: '79ae88bd931ea68464d9' } diff --git a/packages/upload-client/src/index.ts b/packages/upload-client/src/index.ts index 395bc54b1..0b2e829f8 100644 --- a/packages/upload-client/src/index.ts +++ b/packages/upload-client/src/index.ts @@ -21,7 +21,11 @@ export { default as uploadFileGroup } from './uploadFileGroup' /* Helpers */ export { default as UploadClient } from './UploadClient' -export { getUserAgent, GetUserAgentOptions } from '@uploadcare/api-client-utils' +export { + getUserAgent, + GetUserAgentOptions, + UploadcareNetworkError +} from '@uploadcare/api-client-utils' /* Types */ export { UploadcareFile } from './tools/UploadcareFile' diff --git a/packages/upload-client/src/request/request.browser.ts b/packages/upload-client/src/request/request.browser.ts index 4acd64956..de0cd4fad 100644 --- a/packages/upload-client/src/request/request.browser.ts +++ b/packages/upload-client/src/request/request.browser.ts @@ -1,6 +1,7 @@ import { cancelError } from '../tools/errors' import { onCancel } from '../tools/onCancel' import { RequestOptions, RequestResponse } from './types' +import { UploadcareNetworkError } from '@uploadcare/api-client-utils' const request = ({ method, @@ -87,11 +88,11 @@ const request = ({ } } - xhr.onerror = (): void => { + xhr.onerror = (progressEvent): void => { if (aborted) return // only triggers if the request couldn't be made at all - reject(new Error('Network error')) + reject(new UploadcareNetworkError(progressEvent)) } if (onProgress && typeof onProgress === 'function') { diff --git a/packages/upload-client/src/tools/isReadyPoll.ts b/packages/upload-client/src/tools/isReadyPoll.ts index ad47c87db..f3a366e22 100644 --- a/packages/upload-client/src/tools/isReadyPoll.ts +++ b/packages/upload-client/src/tools/isReadyPoll.ts @@ -15,6 +15,7 @@ type ArgsIsReadyPool = { integration?: string userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number onProgress?: ProgressCallback signal?: AbortSignal } @@ -27,6 +28,7 @@ function isReadyPoll({ integration, userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, signal, onProgress }: ArgsIsReadyPool): FileInfo | PromiseLike { @@ -39,7 +41,8 @@ function isReadyPoll({ source, integration, userAgent, - retryThrottledRequestMaxTimes + retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes }).then((response) => { if (response.isReady) { return response diff --git a/packages/upload-client/src/tools/retryIfFailed.ts b/packages/upload-client/src/tools/retryIfFailed.ts new file mode 100644 index 000000000..f097a7801 --- /dev/null +++ b/packages/upload-client/src/tools/retryIfFailed.ts @@ -0,0 +1,48 @@ +import { UploadClientError } from './errors' +import { retrier, UploadcareNetworkError } from '@uploadcare/api-client-utils' + +const REQUEST_WAS_THROTTLED_CODE = 'RequestThrottledError' +const DEFAULT_THROTTLED_TIMEOUT = 15000 +const DEFAULT_NETWORK_ERROR_TIMEOUT = 1000 + +function getTimeoutFromThrottledRequest(error: UploadClientError): number { + const { headers } = error || {} + + return ( + (headers && + Number.parseInt(headers['x-throttle-wait-seconds'] as string) * 1000) || + DEFAULT_THROTTLED_TIMEOUT + ) +} + +type RetryIfFailedOptions = { + retryThrottledRequestMaxTimes: number + retryNetworkErrorMaxTimes: number +} + +export function retryIfFailed( + fn: () => Promise, + options: RetryIfFailedOptions +): Promise { + const { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes } = options + return retrier(({ attempt, retry }) => + fn().catch((error: Error | UploadClientError | UploadcareNetworkError) => { + if ( + 'response' in error && + error?.code === REQUEST_WAS_THROTTLED_CODE && + attempt < retryThrottledRequestMaxTimes + ) { + return retry(getTimeoutFromThrottledRequest(error)) + } + + if ( + error instanceof UploadcareNetworkError && + attempt < retryNetworkErrorMaxTimes + ) { + return retry((attempt + 1) * DEFAULT_NETWORK_ERROR_TIMEOUT) + } + + throw error + }) + ) +} diff --git a/packages/upload-client/src/tools/retryIfThrottled.ts b/packages/upload-client/src/tools/retryIfThrottled.ts deleted file mode 100644 index 8aa6a3009..000000000 --- a/packages/upload-client/src/tools/retryIfThrottled.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { UploadClientError } from './errors' -import { retrier } from '@uploadcare/api-client-utils' - -const REQUEST_WAS_THROTTLED_CODE = 'RequestThrottledError' -const DEFAULT_RETRY_AFTER_TIMEOUT = 15000 - -function getTimeoutFromThrottledRequest(error: UploadClientError): number { - const { headers } = error || {} - - return ( - (headers && - Number.parseInt(headers['x-throttle-wait-seconds'] as string) * 1000) || - DEFAULT_RETRY_AFTER_TIMEOUT - ) -} - -function retryIfThrottled( - fn: () => Promise, - retryThrottledMaxTimes: number -): Promise { - return retrier(({ attempt, retry }) => - fn().catch((error: Error | UploadClientError) => { - if ( - 'response' in error && - error?.code === REQUEST_WAS_THROTTLED_CODE && - attempt < retryThrottledMaxTimes - ) { - return retry(getTimeoutFromThrottledRequest(error)) - } - - throw error - }) - ) -} - -export default retryIfThrottled diff --git a/packages/upload-client/src/types.ts b/packages/upload-client/src/types.ts index 59a2e14b7..bed0fa88b 100644 --- a/packages/upload-client/src/types.ts +++ b/packages/upload-client/src/types.ts @@ -5,11 +5,11 @@ export interface DefaultSettings { baseURL: string maxContentLength: number retryThrottledRequestMaxTimes: number + retryNetworkErrorMaxTimes: number multipartMinFileSize: number multipartChunkSize: number multipartMinLastPartSize: number maxConcurrentRequests: number - multipartMaxAttempts: number pollingTimeoutMilliseconds: number pusherKey: string } diff --git a/packages/upload-client/src/uploadFile/index.ts b/packages/upload-client/src/uploadFile/index.ts index 21fd83035..c0cfe93dc 100644 --- a/packages/upload-client/src/uploadFile/index.ts +++ b/packages/upload-client/src/uploadFile/index.ts @@ -29,11 +29,11 @@ export type FileFromOptions = { userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number contentType?: string multipartMinFileSize?: number multipartChunkSize?: number - multipartMaxAttempts?: number maxConcurrentRequests?: number baseCDN?: string @@ -68,11 +68,11 @@ function uploadFile( userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, contentType, multipartMinFileSize, multipartChunkSize, - multipartMaxAttempts, maxConcurrentRequests, baseCDN = defaultSettings.baseCDN, @@ -92,7 +92,6 @@ function uploadFile( publicKey, contentType, multipartChunkSize, - multipartMaxAttempts, fileName, baseURL, @@ -109,6 +108,7 @@ function uploadFile( maxConcurrentRequests, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, baseCDN, metadata @@ -133,6 +133,7 @@ function uploadFile( userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, baseCDN, metadata @@ -160,6 +161,7 @@ function uploadFile( userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, pusherKey, metadata }) @@ -180,6 +182,7 @@ function uploadFile( userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, baseCDN }) diff --git a/packages/upload-client/src/uploadFile/uploadDirect.ts b/packages/upload-client/src/uploadFile/uploadDirect.ts index b750361e7..67446e2bd 100644 --- a/packages/upload-client/src/uploadFile/uploadDirect.ts +++ b/packages/upload-client/src/uploadFile/uploadDirect.ts @@ -24,6 +24,7 @@ type DirectOptions = { userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number baseCDN?: string metadata?: Metadata @@ -49,6 +50,7 @@ const uploadDirect = ( userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, baseCDN, metadata @@ -68,6 +70,7 @@ const uploadDirect = ( integration, userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, metadata }) .then(({ file }) => { @@ -79,6 +82,7 @@ const uploadDirect = ( integration, userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, onProgress, signal }) diff --git a/packages/upload-client/src/uploadFile/uploadFromUploaded.ts b/packages/upload-client/src/uploadFile/uploadFromUploaded.ts index a776854d6..e1ea9876e 100644 --- a/packages/upload-client/src/uploadFile/uploadFromUploaded.ts +++ b/packages/upload-client/src/uploadFile/uploadFromUploaded.ts @@ -20,6 +20,7 @@ type FromUploadedOptions = { userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number baseCDN?: string } @@ -36,6 +37,7 @@ const uploadFromUploaded = ( integration, userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, baseCDN }: FromUploadedOptions ): Promise => { @@ -46,7 +48,8 @@ const uploadFromUploaded = ( source, integration, userAgent, - retryThrottledRequestMaxTimes + retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes }) .then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN, fileName })) .then((result) => { diff --git a/packages/upload-client/src/uploadFile/uploadFromUrl.ts b/packages/upload-client/src/uploadFile/uploadFromUrl.ts index 7bbdc7b7d..2db528084 100644 --- a/packages/upload-client/src/uploadFile/uploadFromUrl.ts +++ b/packages/upload-client/src/uploadFile/uploadFromUrl.ts @@ -20,6 +20,7 @@ function pollStrategy({ integration, userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, onProgress, signal }: { @@ -29,6 +30,7 @@ function pollStrategy({ integration?: string userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number onProgress?: ProgressCallback signal?: AbortSignal }): Promise { @@ -40,6 +42,7 @@ function pollStrategy({ integration, userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, signal }).then((response) => { switch (response.status) { diff --git a/packages/upload-client/src/uploadFile/uploadMultipart.ts b/packages/upload-client/src/uploadFile/uploadMultipart.ts index beec689a3..bfce18d45 100644 --- a/packages/upload-client/src/uploadFile/uploadMultipart.ts +++ b/packages/upload-client/src/uploadFile/uploadMultipart.ts @@ -10,7 +10,7 @@ import runWithConcurrency from '../tools/runWithConcurrency' import { UploadcareFile } from '../tools/UploadcareFile' import { getFileSize } from '../tools/isMultipart' import { isReadyPoll } from '../tools/isReadyPoll' -import { retrier, CustomUserAgent } from '@uploadcare/api-client-utils' +import { CustomUserAgent } from '@uploadcare/api-client-utils' import { ComputableProgressInfo, @@ -36,13 +36,13 @@ export type MultipartOptions = { integration?: string userAgent?: CustomUserAgent retryThrottledRequestMaxTimes?: number + retryNetworkErrorMaxTimes?: number maxConcurrentRequests?: number - multipartMaxAttempts?: number baseCDN?: string metadata?: Metadata } -const uploadPartWithRetry = ( +const uploadPart = ( chunk: Buffer | Blob, url: string, { @@ -50,23 +50,18 @@ const uploadPartWithRetry = ( onProgress, signal, integration, - multipartMaxAttempts - }: MultipartUploadOptions & { multipartMaxAttempts: number } + retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes + }: MultipartUploadOptions ): Promise => - retrier(({ attempt, retry }) => - multipartUpload(chunk, url, { - publicKey, - onProgress, - signal, - integration - }).catch((error) => { - if (attempt < multipartMaxAttempts) { - return retry() - } - - throw error - }) - ) + multipartUpload(chunk, url, { + publicKey, + onProgress, + signal, + integration, + retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes + }) const uploadMultipart = ( file: NodeFile | BrowserFile, @@ -88,11 +83,11 @@ const uploadMultipart = ( userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, contentType, multipartChunkSize = defaultSettings.multipartChunkSize, maxConcurrentRequests = defaultSettings.maxConcurrentRequests, - multipartMaxAttempts = defaultSettings.multipartMaxAttempts, baseCDN, metadata @@ -138,6 +133,7 @@ const uploadMultipart = ( integration, userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, metadata }) .then(({ uuid, parts }) => { @@ -148,12 +144,13 @@ const uploadMultipart = ( maxConcurrentRequests, parts.map( (url, index) => (): Promise => - uploadPartWithRetry(getChunk(index), url, { + uploadPart(getChunk(index), url, { publicKey, onProgress: createProgressHandler(parts.length, index), signal, integration, - multipartMaxAttempts + retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes }) ) ) @@ -166,7 +163,8 @@ const uploadMultipart = ( source, integration, userAgent, - retryThrottledRequestMaxTimes + retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes }) ) .then((fileInfo) => { @@ -181,6 +179,7 @@ const uploadMultipart = ( integration, userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, onProgress, signal }) diff --git a/packages/upload-client/src/uploadFileGroup/index.ts b/packages/upload-client/src/uploadFileGroup/index.ts index 371c92feb..9bad65aed 100644 --- a/packages/upload-client/src/uploadFileGroup/index.ts +++ b/packages/upload-client/src/uploadFileGroup/index.ts @@ -38,6 +38,7 @@ export default function uploadFileGroup( userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, contentType, multipartChunkSize = defaultSettings.multipartChunkSize, @@ -97,6 +98,7 @@ export default function uploadFileGroup( userAgent, retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes, contentType, multipartChunkSize, @@ -117,7 +119,8 @@ export default function uploadFileGroup( source, integration, userAgent, - retryThrottledRequestMaxTimes + retryThrottledRequestMaxTimes, + retryNetworkErrorMaxTimes }) .then((groupInfo) => new UploadcareGroup(groupInfo, files)) .then((group) => { diff --git a/packages/upload-client/src/version.ts b/packages/upload-client/src/version.ts index bfbcd7dc1..50f03b40d 100644 --- a/packages/upload-client/src/version.ts +++ b/packages/upload-client/src/version.ts @@ -1 +1 @@ -export default '4.3.1' +export default '5.0.0' diff --git a/packages/upload-client/test/tools/retryIfFailed.test.ts b/packages/upload-client/test/tools/retryIfFailed.test.ts new file mode 100644 index 000000000..8da38ed9a --- /dev/null +++ b/packages/upload-client/test/tools/retryIfFailed.test.ts @@ -0,0 +1,206 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { retryIfFailed } from '../../src/tools/retryIfFailed' +import { UploadClientError } from '../../src/tools/errors' +import { jest, expect } from '@jest/globals' +import { UploadcareNetworkError } from '@uploadcare/api-client-utils' + +const createRunner = ({ + attempts = 10, + error, + resolve = 0 +}: { + attempts?: number + error: Error + resolve?: number +}) => { + let runs = 0 + const spy = jest.fn() + + const task = () => + Promise.resolve().then(() => { + ++runs + + spy() + + if (runs <= attempts) { + throw error + } + + return resolve + }) + + return { spy, task } +} + +const throttledError = new UploadClientError( + 'test error', + 'RequestThrottledError', + undefined, + { + error: { + statusCode: 429, + content: 'test', + errorCode: 'RequestThrottledError' + } + }, + { 'x-throttle-wait-seconds': '1' } +) + +const networkError = new UploadcareNetworkError( + new Event('ProgressEvent') as ProgressEvent +) + +describe('retryIfFailed', () => { + describe('Throttle errors', () => { + it('should work', async () => { + const { spy, task } = createRunner({ attempts: 1, error: throttledError }) + + await expect( + retryIfFailed(task, { + retryThrottledRequestMaxTimes: 10, + retryNetworkErrorMaxTimes: 0 + }) + ).resolves.toBe(0) + expect(spy).toHaveBeenCalledTimes(2) + }) + + it('should be rejected with error if not throttled', async () => { + const error = new Error() + const { spy, task } = createRunner({ error }) + + await expect( + retryIfFailed(task, { + retryThrottledRequestMaxTimes: 2, + retryNetworkErrorMaxTimes: 0 + }) + ).rejects.toThrowError(error) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should be rejected with UploadClientError if MaxTimes = 0', async () => { + const { spy, task } = createRunner({ error: throttledError }) + + await expect( + retryIfFailed(task, { + retryThrottledRequestMaxTimes: 0, + retryNetworkErrorMaxTimes: 0 + }) + ).rejects.toThrowError(UploadClientError) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should resolve if task resolve', async () => { + const { spy, task } = createRunner({ + error: throttledError, + attempts: 3, + resolve: 100 + }) + + await expect( + retryIfFailed(task, { + retryThrottledRequestMaxTimes: 10, + retryNetworkErrorMaxTimes: 0 + }) + ).resolves.toBe(100) + expect(spy).toHaveBeenCalledTimes(4) + }) + + it('should resolve without errors if task resolve', async () => { + const { spy, task } = createRunner({ error: throttledError, attempts: 0 }) + + await expect( + retryIfFailed(task, { + retryThrottledRequestMaxTimes: 10, + retryNetworkErrorMaxTimes: 0 + }) + ).resolves.toBe(0) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + + describe('Network errors', () => { + it('should work', async () => { + const { spy, task } = createRunner({ attempts: 1, error: networkError }) + + await expect( + retryIfFailed(task, { + retryNetworkErrorMaxTimes: 10, + retryThrottledRequestMaxTimes: 0 + }) + ).resolves.toBe(0) + expect(spy).toHaveBeenCalledTimes(2) + }) + + it('should be rejected with error if no network error', async () => { + const error = new Error() + const { spy, task } = createRunner({ error }) + + await expect( + retryIfFailed(task, { + retryNetworkErrorMaxTimes: 2, + retryThrottledRequestMaxTimes: 0 + }) + ).rejects.toThrowError(error) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should be rejected with UploadcareNetworkError if MaxTimes = 0', async () => { + const { spy, task } = createRunner({ error: networkError }) + + await expect( + retryIfFailed(task, { + retryNetworkErrorMaxTimes: 0, + retryThrottledRequestMaxTimes: 0 + }) + ).rejects.toThrowError(UploadcareNetworkError) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should resolve if task resolve', async () => { + const { spy, task } = createRunner({ + error: networkError, + attempts: 3, + resolve: 100 + }) + + await expect( + retryIfFailed(task, { + retryNetworkErrorMaxTimes: 10, + retryThrottledRequestMaxTimes: 0 + }) + ).resolves.toBe(100) + expect(spy).toHaveBeenCalledTimes(4) + }) + + it('should resolve without errors if task resolve', async () => { + const { spy, task } = createRunner({ error: networkError, attempts: 0 }) + + await expect( + retryIfFailed(task, { + retryNetworkErrorMaxTimes: 10, + retryThrottledRequestMaxTimes: 0 + }) + ).resolves.toBe(0) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should increase timeout by 1 second on each attempt', async () => { + const { task } = createRunner({ error: networkError, attempts: 4 }) + + const start = Date.now() + await expect( + retryIfFailed(task, { + retryNetworkErrorMaxTimes: 10, + retryThrottledRequestMaxTimes: 0 + }) + ).resolves.toBe(0) + const end = Date.now() + const diff = end - start + + // 1+2+3+4=10 + expect(diff).toBeGreaterThan(10000) + // expect max ~4s spent on doing requests, it could be slow on CI and needs to be tested + expect(diff).toBeLessThan(14000) + }) + }) +}) diff --git a/packages/upload-client/test/tools/retryIfThrottled.test.ts b/packages/upload-client/test/tools/retryIfThrottled.test.ts deleted file mode 100644 index e0196b28c..000000000 --- a/packages/upload-client/test/tools/retryIfThrottled.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import retryIfThrottled from '../../src/tools/retryIfThrottled' -import { UploadClientError } from '../../src/tools/errors' -import { jest, expect } from '@jest/globals' - -const createRunner = ({ - attempts = 10, - error, - resolve = 0 -}: { attempts?: number; error?: Error; resolve?: number } = {}) => { - let runs = 0 - const spy = jest.fn() - - const task = () => - Promise.resolve().then(() => { - ++runs - - spy() - - if (runs <= attempts) { - throw error - ? error - : new UploadClientError( - 'test error', - 'RequestThrottledError', - undefined, - { - error: { - statusCode: 429, - content: 'test', - errorCode: 'RequestThrottledError' - } - }, - { 'x-throttle-wait-seconds': '1' } - ) - } - - return resolve - }) - - return { spy, task } -} - -describe('retryIfThrottled', () => { - it('should work', async () => { - const { spy, task } = createRunner({ attempts: 1 }) - - await expect(retryIfThrottled(task, 10)).resolves.toBe(0) - expect(spy).toHaveBeenCalledTimes(2) - }) - - it('should rejected with error if no Throttled', async () => { - const error = new Error() - const { spy, task } = createRunner({ error }) - - await expect(retryIfThrottled(task, 2)).rejects.toThrowError(error) - expect(spy).toHaveBeenCalledTimes(1) - }) - - it('should rejected with UploadClientError if MaxTimes = 0', async () => { - const { spy, task } = createRunner() - - await expect(retryIfThrottled(task, 0)).rejects.toThrowError( - UploadClientError - ) - expect(spy).toHaveBeenCalledTimes(1) - }) - - it('should resolve if task resolve', async () => { - const { spy, task } = createRunner({ attempts: 3, resolve: 100 }) - - await expect(retryIfThrottled(task, 10)).resolves.toBe(100) - expect(spy).toHaveBeenCalledTimes(4) - }) - - it('should resolve without errors if task resolve', async () => { - const { spy, task } = createRunner({ attempts: 0 }) - - await expect(retryIfThrottled(task, 10)).resolves.toBe(0) - expect(spy).toHaveBeenCalledTimes(1) - }) -})