axios-retryer
Version:
TypeScript-first Axios retry library with concurrency limits, request priority, token refresh, response caching, and circuit breaker plugins.
2 lines (1 loc) • 16.4 kB
JavaScript
"use strict";var e=require("axios");class t extends Error{constructor(e,t){super(e),this.name=new.target.name,this.code=t,Object.setPrototypeOf(this,new.target.prototype)}}class r extends t{constructor(e,t,r){super(e,"EINVALID_CONFIG"),this.optionName=t,this.optionValue=r}}class s{constructor(){this.activeTimers=new Set,this.activeSleepRejects=new Set,this.isDestroyed=!1}createTimeout(e,t){if(this.isDestroyed)return{timerId:null,cancel:()=>{}};const r=setTimeout(()=>{this.activeTimers.delete(r),this.isDestroyed||e()},t);return this.activeTimers.add(r),{timerId:r,cancel:()=>{this.activeTimers.has(r)&&(clearTimeout(r),this.activeTimers.delete(r))}}}createSleep(e){let r=()=>{};return{promise:new Promise((s,i)=>{if(this.isDestroyed)return void i(new t("Sleep cancelled","ETIMER_CANCELLED"));const n=e=>{this.activeSleepRejects.delete(n),i(e)};this.activeSleepRejects.add(n);const{cancel:o}=this.createTimeout(()=>{this.activeSleepRejects.delete(n),s()},e);r=()=>{o(),n(new t("Sleep cancelled","ETIMER_CANCELLED"))}}),cancel:r}}getActiveTimerCount(){return this.activeTimers.size}destroy(){this.isDestroyed=!0,this.activeTimers.forEach(e=>{clearTimeout(e)}),this.activeTimers.clear();const e=new t("Sleep cancelled","ETIMER_CANCELLED");this.activeSleepRejects.forEach(t=>t(e)),this.activeSleepRejects.clear()}}class i extends t{constructor(e="No token refresh handler provided"){super(e,"EMISSING_TOKEN_REFRESH_HANDLER")}}class n extends t{constructor(e="Token refresh failed"){super(e,"ETOKEN_REFRESH_FAILED")}}class o extends t{constructor(e="Token refresh timeout"){super(e,"ETOKEN_REFRESH_TIMEOUT"),this.retryableRefreshFailure=!0}}class h extends t{constructor(e="Token refresh aborted"){super(e,"ETOKEN_REFRESH_ABORTED"),this.stopRefreshRetries=!0}}function u(e){return e instanceof h||"object"==typeof e&&null!==e&&"stopRefreshRetries"in e&&!0===e.stopRefreshRetries}function a(t){return!(t instanceof h||!e.isAxiosError(t)&&("object"!=typeof t||null===t||!0!==t.retryableRefreshFailure&&!0!==t.isAxiosError&&!("request"in t)&&!("response"in t)))}function c(e){if(e instanceof Error)return e;if("object"==typeof e&&null!==e){const t=e;return function(e,t){const r=e;return"string"==typeof t.code&&t.code.length>0&&(r.code=t.code),"cause"in t&&(r.cause=t.cause),"request"in t&&(r.request=t.request),"response"in t&&(r.response=t.response),"status"in t&&(r.status=t.status),e}(new n("string"==typeof t.message&&t.message.length>0?t.message:"Token refresh failed"),t)}return"string"==typeof e&&e.length>0?new n(e):new n}class l extends t{constructor(e){super(`Token refresh queue overflowed: ${e} requests pending.`,"ETOKEN_REFRESH_QUEUE_OVERFLOW"),this.queueSize=e}}const f={maxRefreshAttempts:3,authHeaderName:"Authorization",refreshStatusCodes:[401],refreshTimeout:15e3,retryOnRefreshFail:!0,tokenPrefix:"Bearer ",maxRefreshBackoffMs:3e4,maxQueuedRequests:500};class d{constructor(){this.teardownError=null,this.listeners=new Set}get error(){return this.teardownError}ensureActive(){if(this.teardownError)throw this.teardownError}wrap(e){return this.teardownError?Promise.reject(this.teardownError):new Promise((t,r)=>{let s=!1;const i=e=>{s||(s=!0,r(e))},n=e=>{i(e)};this.listeners.add(n),e.then(e=>{s||(s=!0,t(e))},i).finally(()=>{this.listeners.delete(n)})})}dispose(e,t){this.teardownError||(this.teardownError=e,this.listeners.forEach(t=>t(e)),this.listeners.clear(),null==t||t())}reset(){this.teardownError=null,this.listeners.clear()}}class p{constructor(e){this.options=e}async run(){const{teardown:e,refreshToken:t,pluginName:r,getLogger:s}=this.options;if(e.ensureActive(),!t)throw new i;const n=s(),{maxRefreshAttempts:h,refreshTimeout:l,retryOnRefreshFail:f,maxRefreshBackoffMs:d}=this.options;let p;for(let s=1;s<=h;s++){e.ensureActive(),null==n||n.debug(`[${r}] Refresh attempt ${s}/${h}`);try{const s=new Promise((e,r)=>{const{cancel:s}=this.options.timerManager.createTimeout(()=>r(new o("Token refresh timeout")),l);t(this.options.refreshAxios).then(t=>{s(),e(t)}).catch(e=>{s(),r(e)})}),{token:i}=await e.wrap(s);return e.ensureActive(),null==i?(null==n||n.debug(`[${r}] Refresh handler returned no token; skipping refresh`),null):(this.options.onRefreshSuccess(i),null==n||n.debug(`[${r}] Token successfully refreshed`),i)}catch(t){if(p=c(t),u(t)){null==n||n.debug(`[${r}] Refresh retries aborted by refresh handler`,{attempt:s,reason:p.message});break}if(!a(t)){null==n||n.debug(`[${r}] Refresh retries stopped after a terminal refresh error`,{attempt:s,reason:p.message});break}if(!f)break;if(s<h){const t=Math.min(1e3*2**(s-1),d);null==n||n.debug(`[${r}] Refresh attempt failed, retrying in ${t}ms...`);const{promise:i}=this.options.timerManager.createSleep(t);await e.wrap(i);continue}break}}throw p}}function R(e,t){const r=e.headers;if(!r)return!1;if("function"==typeof r.has&&r.has(t))return!0;if("function"==typeof r.get){const e=r.get(t);if(null!=e&&!1!==e)return!0}const s=t.toLowerCase(),i=r[t];if(null!=i&&!1!==i)return!0;const n=r[s];return null!=n&&!1!==n||Object.entries(r).some(([e,t])=>e.toLowerCase()===s&&null!=t&&!1!==t)}function m(e,t){var r;const s=e.headers;if(!s)return null;if("function"==typeof s.get){const e=s.get(t);if("string"==typeof e)return e}const i=t.toLowerCase(),n=null!==(r=s[t])&&void 0!==r?r:s[i];if("string"==typeof n)return n;const o=Object.entries(s).find(([e])=>e.toLowerCase()===i);return"string"==typeof(null==o?void 0:o[1])?o[1]:null}function g(e,t,r){e.headers||(e.headers={}),"function"!=typeof e.headers.set?e.headers[t]=r:e.headers.set(t,r)}function E(e){return e.replace(/[\r\n\0\u2028\u2029]/g,"")}const w=new Set(["retryAttempt","requestRetries","requestMode","requestId","correlationId","isRetrying","priority","timestamp","backoffType","retryableStatuses","extra","isRetryRefreshRequest","manualReplayAttempt","retryAfterMs","silentlyCancelled","cachingOptions"]);function y(e){return w.has(e)&&"__proto__"!==e&&"constructor"!==e&&"prototype"!==e}function T(e){const t={};if(Object.defineProperty(t,"toJSON",{value:()=>{},enumerable:!1,writable:!1,configurable:!1}),e)for(const r of Object.keys(e))y(r)&&(t[r]=e[r]);return t}function v(e){const t=e;return t.__axiosRetryer?"function"!=typeof t.__axiosRetryer.toJSON&&(t.__axiosRetryer=T(t.__axiosRetryer)):t.__axiosRetryer=T(),t.__axiosRetryer}function x(e){if(e)return e.__axiosRetryer}class k{constructor(e,t){if(this.name="TokenRefreshPlugin",this.version="1.0.0",this.requestInterceptorId=null,this.interceptorId=null,this.responseInterceptorId=null,this.isRefreshing=!1,this.refreshQueue=[],this.teardown=new d,this.timerManager=new s,this.failedAuthHeaderValue=null,this.logger=null,this.refreshToken=e,this.options=function(e){return{...f,...e}}(t),!Number.isInteger(this.options.maxRefreshAttempts)||this.options.maxRefreshAttempts<1)throw new r("maxRefreshAttempts must be a positive integer","maxRefreshAttempts",this.options.maxRefreshAttempts);if(!Number.isInteger(this.options.refreshTimeout)||this.options.refreshTimeout<1)throw new r("refreshTimeout must be a positive integer","refreshTimeout",this.options.refreshTimeout)}initialize(t){this.timerManager.destroy(),this.timerManager=new s,this.context=t,this.isRefreshing=!1,this.refreshQueue=[],this.teardown.reset(),this.failedAuthHeaderValue=null,this.refreshAxios=function(t,r){const s=t.axiosInstance.defaults;return e.create({adapter:s.adapter,baseURL:s.baseURL,timeout:r,withCredentials:s.withCredentials,httpAgent:s.httpAgent,httpsAgent:s.httpsAgent,proxy:s.proxy,socketPath:s.socketPath,maxRedirects:s.maxRedirects})}(t,this.options.refreshTimeout),this.logger=t.getLogger(),this.interceptorId=t.axiosInstance.interceptors.response.use(e=>e,e=>this.handleResponseError(e)),this.options.customErrorDetector&&(this.responseInterceptorId=t.axiosInstance.interceptors.response.use(e=>this.handleSuccessResponse(e),e=>Promise.reject(e))),this.requestInterceptorId=this.context.axiosInstance.interceptors.request.use(async e=>{const t=v(e);t.manualReplayAttempt&&(this.failedAuthHeaderValue=null,function(e){delete v(e).manualReplayAttempt}(e));const{authHeaderName:r}=this.options,s=this.context.axiosInstance.defaults.headers.common[r];if(!t.isRetryRefreshRequest&&R(e,r)){if(this.isRefreshing)return this.isQueueOverflowing()?(this.context.releaseRequestTracking(e),Promise.reject(this.bindRefreshErrorToRequest(this.buildQueueOverflowError(),e))):new Promise((t,r)=>{this.refreshQueue.push({kind:"hold-request",config:e,resolveConfig:e=>t(e),reject:r})});const t=m(e,r);if(null!==this.failedAuthHeaderValue&&null!==t&&function(e,t){if(e.length!==t.length)return!1;let r=0;for(let s=0;s<e.length;s++)r|=e.charCodeAt(s)^t.charCodeAt(s);return 0===r}(t,this.failedAuthHeaderValue))return this.context.releaseRequestTracking(e),Promise.reject(this.bindRefreshErrorToRequest(new n,e))}return s&&R(e,r)&&g(e,r,String(s)),e})}onBeforeDestroyed(e){this.dispose(new h("Token refresh aborted because the plugin was destroyed")),null!==this.requestInterceptorId&&(e.axiosInstance.interceptors.request.eject(this.requestInterceptorId),this.requestInterceptorId=null),null!==this.interceptorId&&(e.axiosInstance.interceptors.response.eject(this.interceptorId),this.interceptorId=null),null!==this.responseInterceptorId&&(e.axiosInstance.interceptors.response.eject(this.responseInterceptorId),this.responseInterceptorId=null)}withTeardown(e){return this.teardown.wrap(e)}ensureActive(){this.teardown.ensureActive()}bindRefreshErrorToRequest(t,r,s){var i;const n=new e.AxiosError(t.message,"TOKEN_REFRESH_FAILED",r,void 0,s);return n.name=t.name,n.cause=null!==(i=t.cause)&&void 0!==i?i:t,n}isQueueOverflowing(){const e=this.options.maxQueuedRequests;return!(!Number.isFinite(e)||e<=0)&&this.refreshQueue.length>=e}buildQueueOverflowError(){var e;const t=new l(this.refreshQueue.length);return null===(e=this.logger)||void 0===e||e.warn(`[${this.name}] Refresh queue overflow; rejecting incoming request`,{queueSize:this.refreshQueue.length,maxQueuedRequests:this.options.maxQueuedRequests}),t}rejectQueueEntryWithBoundError(e,t){"hold-request"===t.kind?(this.context.releaseRequestTracking(t.config),t.reject(this.bindRefreshErrorToRequest(e,t.config))):t.reject("retry-after-error"===t.kind?this.bindRefreshErrorToRequest(e,t.request,t.sourceError.response):this.bindRefreshErrorToRequest(e,t.response.config,t.response))}dispose(e){this.teardown.error||(this.timerManager.destroy(),this.isRefreshing=!1,this.teardown.dispose(e,()=>{this.refreshQueue.forEach(t=>{this.rejectQueueEntryWithBoundError(e,t)}),this.refreshQueue=[]}))}async handleSuccessResponse(e){var t,r;this.ensureActive();const{customErrorDetector:s}=this.options,i=x(e.config);if((null==i?void 0:i.isRetryRefreshRequest)||!s)return e;let n;try{n=s(e.data)}catch(r){return null===(t=this.logger)||void 0===t||t.warn(`[${this.name}] customErrorDetector threw; ignoring body-auth refresh signal`,{error:r instanceof Error?r.message:r}),e}if(n){if(null===(r=this.logger)||void 0===r||r.debug(`[${this.name}] Custom auth error detected in response body`),this.context.releaseRequestTracking(e.config),this.isRefreshing){if(this.isQueueOverflowing())throw this.bindRefreshErrorToRequest(this.buildQueueOverflowError(),e.config,e);return new Promise((t,r)=>{this.refreshQueue.push({kind:"retry-after-body-auth-error",response:e,resolveResponse:t,reject:r})})}try{this.isRefreshing=!0;const t=await this.withTeardown(this.executeTokenRefresh());return this.ensureActive(),null===t?(this.flushQueuedAfterSkippedRefresh(),e):(this.updateAuthHeader(t),this.flushQueuedWithToken(t),this.withTeardown(this.retryRequest(e.config,t)))}catch(t){const r=this.handleRefreshFailure(t);throw this.bindRefreshErrorToRequest(r,e.config,e)}finally{this.isRefreshing=!1}}return e}async handleResponseError(e){var t,r,s,i,o;const h=e.config;if(!h)return Promise.reject(e);if("TOKEN_REFRESH_FAILED"===e.code)return Promise.reject(e);if(this.ensureActive(),null===(t=x(h))||void 0===t?void 0:t.isRetryRefreshRequest)return Promise.reject(e);if(!this.isRefreshableError(e))return Promise.reject(e);if(null!==this.failedAuthHeaderValue&&m(h,this.options.authHeaderName)===this.failedAuthHeaderValue)return this.context.releaseRequestTracking(h),Promise.reject(this.bindRefreshErrorToRequest(new n,h,e.response));const u=null===(o=null===(i=null===(s=null===(r=this.context.axiosInstance)||void 0===r?void 0:r.defaults)||void 0===s?void 0:s.headers)||void 0===i?void 0:i.common)||void 0===o?void 0:o[this.options.authHeaderName],a=m(h,this.options.authHeaderName);if("string"==typeof u&&"string"==typeof a&&u.length>0&&a.length>0&&u!==a){const e=(c=u).startsWith(l=this.options.tokenPrefix)?c.slice(l.length):c;return this.withTeardown(this.retryRequest(h,e))}var c,l;return this.context.releaseRequestTracking(h),this.isRefreshing?this.queueRefreshRequest(h,e):this.handleTokenRefresh(h,e)}isRefreshableError(e){var t,r;const s=null!==(r=null===(t=e.response)||void 0===t?void 0:t.status)&&void 0!==r?r:-1;return this.options.refreshStatusCodes.includes(s)}async handleTokenRefresh(e,t){var r;this.isRefreshing=!0,(null===(r=x(e))||void 0===r?void 0:r.isRetryRefreshRequest)||this.context.triggerAndEmit("onBeforeTokenRefresh");try{const r=await this.executeTokenRefresh();return this.ensureActive(),null===r?(this.flushQueuedAfterSkippedRefresh(),Promise.reject(t)):(this.updateAuthHeader(r),this.flushQueuedWithToken(r),this.withTeardown(this.retryRequest(e,r)))}catch(r){const s=this.handleRefreshFailure(r);return Promise.reject(this.bindRefreshErrorToRequest(s,e,t.response))}finally{this.isRefreshing=!1}}executeTokenRefresh(){return new p({pluginName:this.name,refreshAxios:this.refreshAxios,refreshToken:this.refreshToken,timerManager:this.timerManager,teardown:this.teardown,maxRefreshAttempts:this.options.maxRefreshAttempts,refreshTimeout:this.options.refreshTimeout,retryOnRefreshFail:this.options.retryOnRefreshFail,maxRefreshBackoffMs:this.options.maxRefreshBackoffMs,getLogger:()=>this.logger,onRefreshSuccess:e=>{this.context.triggerAndEmit("onTokenRefreshed",e)}}).run()}updateAuthHeader(e){const{authHeaderName:t,tokenPrefix:r}=this.options;this.context.axiosInstance.defaults.headers.common[t]=`${r}${E(e)}`,this.failedAuthHeaderValue=null}retryRequest(e,t){const{authHeaderName:r,tokenPrefix:s}=this.options,i={...e,headers:{...e.headers,[r]:`${s}${E(t)}`}};return function(e,t){const r=v(e);for(const e of Object.keys(t)){if(!y(e))continue;const s=t[e];void 0===s?delete r[e]:r[e]=s}}(i,{isRetryRefreshRequest:!0}),this.context.axiosInstance.request(i)}queueRefreshRequest(e,t){return this.isQueueOverflowing()?Promise.reject(this.bindRefreshErrorToRequest(this.buildQueueOverflowError(),e,t.response)):new Promise((r,s)=>{this.refreshQueue.push({kind:"retry-after-error",request:e,sourceError:t,resolveResponse:r,reject:s})})}flushQueuedWithToken(e){const{authHeaderName:t,tokenPrefix:r}=this.options,s=this.refreshQueue;this.refreshQueue=[];for(const i of s)"hold-request"===i.kind?(g(i.config,t,`${r}${E(e)}`),i.resolveConfig(i.config)):"retry-after-error"===i.kind?this.retryRequest(i.request,e).then(i.resolveResponse).catch(i.reject):this.retryRequest(i.response.config,e).then(i.resolveResponse).catch(i.reject)}flushQueuedAfterSkippedRefresh(){const e=this.refreshQueue;this.refreshQueue=[];for(const t of e)"hold-request"===t.kind?t.resolveConfig(t.config):"retry-after-error"===t.kind?t.reject(t.sourceError):t.resolveResponse(t.response)}handleRefreshFailure(t){var r,s,i,o,h;let l;this.teardown.error&&(t=this.teardown.error),u(t)||!a(t)?l=c(t):t instanceof e.AxiosError?(l=new n,l.cause=t):l=c(t),this.refreshQueue.forEach(e=>{this.rejectQueueEntryWithBoundError(l,e)}),this.refreshQueue=[];const f=null===(o=null===(i=null===(s=null===(r=this.context.axiosInstance)||void 0===r?void 0:r.defaults)||void 0===s?void 0:s.headers)||void 0===i?void 0:i.common)||void 0===o?void 0:o[this.options.authHeaderName];return"string"==typeof f&&(this.failedAuthHeaderValue=f),this.context.triggerAndEmit("onTokenRefreshFailed"),null===(h=this.logger)||void 0===h||h.error(`${this.name} Token refresh failed - clearing queue`,{reason:l.message,aborted:u(t)||!a(t)}),l}}exports.MissingTokenRefreshHandlerError=i,exports.TokenRefreshAbortError=h,exports.TokenRefreshFailedError=n,exports.TokenRefreshPlugin=k,exports.TokenRefreshQueueOverflowError=l,exports.TokenRefreshTimeoutError=o,exports.createTokenRefreshPlugin=function(e,t){return new k(e,t)};