1 import { OFFLINE_RETRY_ATTEMPTS_MAX, OFFLINE_RETRY_DELAY, RETRY_ATTEMPTS_MAX, RETRY_DELAY_MAX } from '../../constants';
2 import { API_CUSTOM_ERROR_CODES, HTTP_ERROR_CODES } from '../../errors';
4 getDeviceVerificationHeaders,
6 getVerificationHeaders,
8 } from '../../fetch/headers';
9 import { getDateHeader } from '../../fetch/helpers';
10 import { wait } from '../../helpers/promise';
11 import { setRefreshCookies } from '../auth';
12 import { getApiError } from './apiErrorHelper';
13 import { createDeviceHandlers } from './deviceVerificationHandler';
14 import { AppVersionBadError, InactiveSessionError } from './errors';
15 import { createRefreshHandlers, getIsRefreshFailure, refresh } from './refreshHandlers';
16 import { retryHandler } from './retryHandler';
19 * Attach a catch handler to every API call to handle 401, 403, and other errors.
21 export default ({ call, onMissingScopes, onVerification }) => {
22 let loggedOut = false;
23 let appVersionBad = false;
25 const refreshHandler = createRefreshHandlers((UID) => {
26 return refresh(() => call(withUIDHeaders(UID, setRefreshCookies())), 1, RETRY_ATTEMPTS_MAX);
29 const deviceVerificationHandler = createDeviceHandlers();
33 const cb = (options) => {
34 const perform = (attempts, maxAttempts) => {
36 return Promise.reject(InactiveSessionError());
39 return Promise.reject(AppVersionBadError());
42 return call(options).catch((e) => {
44 throw InactiveSessionError();
47 if (maxAttempts && attempts >= maxAttempts) {
51 const { status, name, response } = e;
57 retriesOnOffline = OFFLINE_RETRY_ATTEMPTS_MAX,
58 retriesOnTimeout = OFFLINE_RETRY_ATTEMPTS_MAX,
59 maxRetryWaitSeconds = RETRY_DELAY_MAX,
62 if (name === 'OfflineError') {
63 if (attempts > retriesOnOffline) {
66 return wait(OFFLINE_RETRY_DELAY).then(() => perform(attempts + 1, retriesOnOffline));
69 if (name === 'TimeoutError') {
70 if (attempts > retriesOnTimeout) {
73 return perform(attempts + 1, retriesOnTimeout);
76 const ignoreUnauthorized =
77 Array.isArray(ignoreHandler) && ignoreHandler.includes(HTTP_ERROR_CODES.UNAUTHORIZED);
78 const requestUID = getUIDHeaderValue(headers) ?? UID;
79 // Sending a request with a UID but without an authorization header is when the public app makes
80 // authenticated requests (mostly for persisted sessions), and ignoring "login" or "signup" requests.
82 status === HTTP_ERROR_CODES.UNAUTHORIZED &&
83 !ignoreUnauthorized &&
84 (UID || (requestUID && !headers?.Authorization))
86 return refreshHandler(requestUID, getDateHeader(response && response.headers)).then(
87 () => perform(attempts + 1, RETRY_ATTEMPTS_MAX),
89 if (getIsRefreshFailure(error)) {
90 // Disable any further requests on this session if it was created with a UID and the request was done with the failing UID
91 if (UID && requestUID === UID) {
93 // Inactive session error is only thrown when this error was caused by a logged in session requesting through the same UID
94 // to have a specific error consumers can use
95 throw InactiveSessionError(error);
97 // The original 401 error is thrown to make it more clear that this auth & refresh failure
98 // was caused by an original auth failure and consumers can just check for 401 instead of 4xx
101 // Otherwise, this is not actually an authentication error, it might have failed because the API responds with 5xx, or because the client is offline etc
102 // and as such the error from the refresh call is thrown
108 const ignoreUnlock = Array.isArray(ignoreHandler) && ignoreHandler.includes(HTTP_ERROR_CODES.UNLOCK);
109 if (status === HTTP_ERROR_CODES.UNLOCK && !ignoreUnlock) {
110 const { Details: { MissingScopes: missingScopes = [] } = {} } = e.data || {};
111 return onMissingScopes({
112 scopes: missingScopes,
118 const ignoreTooManyRequests =
119 Array.isArray(ignoreHandler) && ignoreHandler.includes(HTTP_ERROR_CODES.TOO_MANY_REQUESTS);
120 if (status === HTTP_ERROR_CODES.TOO_MANY_REQUESTS && !ignoreTooManyRequests) {
121 return retryHandler(e, maxRetryWaitSeconds).then(() => perform(attempts + 1, RETRY_ATTEMPTS_MAX));
124 const { code } = getApiError(e);
126 if (code === API_CUSTOM_ERROR_CODES.APP_VERSION_BAD) {
127 appVersionBad = true;
128 throw AppVersionBadError();
131 const ignoreHumanVerification =
132 Array.isArray(ignoreHandler) &&
133 ignoreHandler.includes(API_CUSTOM_ERROR_CODES.HUMAN_VERIFICATION_REQUIRED);
134 if (code === API_CUSTOM_ERROR_CODES.HUMAN_VERIFICATION_REQUIRED && !ignoreHumanVerification) {
137 HumanVerificationToken: captchaToken,
138 HumanVerificationMethods: methods = [],
143 const onVerify = (token, tokenType) => {
150 ...(Array.isArray(silence) ? silence : []),
151 API_CUSTOM_ERROR_CODES.TOKEN_INVALID,
155 ...getVerificationHeaders(token, tokenType),
160 return onVerification({ token: captchaToken, methods, onVerify, title, error: e });
163 const ignoreDeviceVerification =
164 Array.isArray(ignoreHandler) &&
165 ignoreHandler.includes(API_CUSTOM_ERROR_CODES.DEVICE_VERIFICATION_REQUIRED);
166 if (code === API_CUSTOM_ERROR_CODES.DEVICE_VERIFICATION_REQUIRED && !ignoreDeviceVerification) {
167 const { Details: { ChallengeType: challengeType, ChallengePayload: challengePayload } = {} } =
169 const requestUID = getUIDHeaderValue(headers) ?? UID;
170 return deviceVerificationHandler(requestUID, challengeType, challengePayload)
176 ...getDeviceVerificationHeaders(result),
191 Object.defineProperties(cb, {