From 1b468d6246dcc0c77e7a3e09778384f8bc8cf341 Mon Sep 17 00:00:00 2001 From: f1cognite Date: Tue, 19 Feb 2019 09:08:00 +0100 Subject: [PATCH] Hard copied retry-axios into repository to support es5 --- package.json | 3 +- src/core.ts | 2 +- src/helpers/retry-axios.ts | 218 +++++++++++++++++++++++++++++++++++++ yarn.lock | 5 - 4 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 src/helpers/retry-axios.ts diff --git a/package.json b/package.json index 3d11fedab00..2e74e48a261 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,7 @@ "dependencies": { "axios": "^0.18.0", "jwt-decode": "^2.2.0", - "query-string": "^5.1.1", - "retry-axios": "^0.4.1" + "query-string": "^5.1.1" }, "publishConfig": { "access": "public", diff --git a/src/core.ts b/src/core.ts index 7a73d8f43f0..a46c3c2c75c 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,7 +1,7 @@ // Copyright 2018 Cognite AS import axios, { AxiosError, AxiosRequestConfig } from 'axios'; -import { attach } from 'retry-axios'; +import { attach } from './helpers/retry-axios'; import { MetadataMap } from './MetadataMap'; /** @hidden */ diff --git a/src/helpers/retry-axios.ts b/src/helpers/retry-axios.ts new file mode 100644 index 00000000000..4db09225b69 --- /dev/null +++ b/src/helpers/retry-axios.ts @@ -0,0 +1,218 @@ +// Code copied from https://github.com/JustinBeckwith/retry-axios because of https://github.com/cognitedata/cognitesdk-js/issues/40 and https://github.com/JustinBeckwith/retry-axios/issues/37 + +import axios, { + AxiosError, + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, +} from 'axios'; + +/** + * Configuration for the Axios `request` method. + */ +export interface RetryConfig { + /** + * The number of times to retry the request. Defaults to 3. + */ + retry?: number; + + /** + * The number of retries already attempted. + */ + currentRetryAttempt?: number; + + /** + * The amount of time to initially delay the retry. Defaults to 100. + */ + retryDelay?: number; + + /** + * The instance of the axios object to which the interceptor is attached. + */ + instance?: AxiosInstance; + + /** + * The HTTP Methods that will be automatically retried. + * Defaults to ['GET','PUT','HEAD','OPTIONS','DELETE'] + */ + httpMethodsToRetry?: string[]; + + /** + * The HTTP response status codes that will automatically be retried. + * Defaults to: [[100, 199], [429, 429], [500, 599]] + */ + statusCodesToRetry?: number[][]; + + /** + * Function to invoke when a retry attempt is made. + */ + onRetryAttempt?: (err: AxiosError) => void; + + /** + * Function to invoke which determines if you should retry + */ + shouldRetry?: (err: AxiosError) => boolean; + + /** + * When there is no response, the number of retries to attempt. Defaults to 2. + */ + noResponseRetries?: number; +} + +export type RaxConfig = { + raxConfig: RetryConfig; +} & AxiosRequestConfig; + +/** + * Attach the interceptor to the Axios instance. + * @param instance The optional Axios instance on which to attach the + * interceptor. + * @returns The id of the interceptor attached to the axios instance. + */ +export function attach(instance?: AxiosInstance) { + instance = instance || axios; + return instance.interceptors.response.use(onFulfilled, onError); +} + +/** + * Eject the Axios interceptor that is providing retry capabilities. + * @param interceptorId The interceptorId provided in the config. + * @param instance The axios instance using this interceptor. + */ +export function detach(interceptorId: number, instance?: AxiosInstance) { + instance = instance || axios; + instance.interceptors.response.eject(interceptorId); +} + +function onFulfilled(res: AxiosResponse) { + return res; +} + +function onError(err: AxiosError) { + const config = (err.config as RaxConfig).raxConfig || {}; + config.currentRetryAttempt = config.currentRetryAttempt || 0; + config.retry = + config.retry === undefined || config.retry === null ? 3 : config.retry; + config.retryDelay = config.retryDelay || 100; + config.instance = config.instance || axios; + config.httpMethodsToRetry = config.httpMethodsToRetry || [ + 'GET', + 'HEAD', + 'PUT', + 'OPTIONS', + 'DELETE', + ]; + config.noResponseRetries = + config.noResponseRetries === undefined || config.noResponseRetries === null + ? 2 + : config.noResponseRetries; + + // If this wasn't in the list of status codes where we want + // to automatically retry, return. + const retryRanges = [ + // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + // 1xx - Retry (Informational, request still processing) + // 2xx - Do not retry (Success) + // 3xx - Do not retry (Redirect) + // 4xx - Do not retry (Client errors) + // 429 - Retry ("Too Many Requests") + // 5xx - Retry (Server errors) + [100, 199], + [429, 429], + [500, 599], + ]; + config.statusCodesToRetry = config.statusCodesToRetry || retryRanges; + + // Put the config back into the err + (err.config as RaxConfig).raxConfig = config; + + // Determine if we should retry the request + const shouldRetryFn = config.shouldRetry || shouldRetryRequest; + if (!shouldRetryFn(err)) { + return Promise.reject(err); + } + + // Calculate time to wait with exponential backoff. + // Formula: (2^c - 1 / 2) * 1000 + const delay = ((Math.pow(2, config.currentRetryAttempt) - 1) / 2) * 1000; + + // We're going to retry! Incremenent the counter. + (err.config as RaxConfig).raxConfig!.currentRetryAttempt! += 1; + + // Create a promise that invokes the retry after the backOffDelay + const backoff = new Promise(resolve => { + setTimeout(resolve, delay); + }); + + // Notify the user if they added an `onRetryAttempt` handler + if (config.onRetryAttempt) { + config.onRetryAttempt(err); + } + + // Return the promise in which recalls axios to retry the request + return backoff.then(() => config.instance!.request(err.config)); +} + +/** + * Determine based on config if we should retry the request. + * @param err The AxiosError passed to the interceptor. + */ +function shouldRetryRequest(err: AxiosError) { + const config = (err.config as RaxConfig).raxConfig; + + // If there's no config, or retries are disabled, return. + if (!config || config.retry === 0) { + return false; + } + + // Check if this error has no response (ETIMEDOUT, ENOTFOUND, etc) + if ( + !err.response && + (config.currentRetryAttempt || 0) >= config.noResponseRetries! + ) { + return false; + } + + // Only retry with configured HttpMethods. + if ( + !err.config.method || + config.httpMethodsToRetry!.indexOf(err.config.method.toUpperCase()) < 0 + ) { + return false; + } + + // If this wasn't in the list of status codes where we want + // to automatically retry, return. + if (err.response && err.response.status) { + let isInRange = false; + for (const [min, max] of config.statusCodesToRetry!) { + const status = err.response.status; + if (status >= min && status <= max) { + isInRange = true; + break; + } + } + if (!isInRange) { + return false; + } + } + + // If we are out of retry attempts, return + config.currentRetryAttempt = config.currentRetryAttempt || 0; + if (config.currentRetryAttempt >= config.retry!) { + return false; + } + + return true; +} + +/** + * Acquire the raxConfig object from an AxiosError if available. + * @param err The Axios error with a config object. + */ +export function getConfig(err: AxiosError) { + if (err && err.config) { + return (err.config as RaxConfig).raxConfig; + } + return; +} diff --git a/yarn.lock b/yarn.lock index 793c0ed6720..6a85b28a630 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3045,11 +3045,6 @@ ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" -retry-axios@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-0.4.1.tgz#22ee392c6f20ae858d06650fa06091ea4f3406e5" - integrity sha512-h3mdzDUw4MlvzjxJ96mysapyxcHzAmGVywrBtU5oAtXI2aBxMEgcmyepkKfoVXK6we2padRl1BwwvL1N7+lPwA== - rimraf@^2.5.4, rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"