1 import { type ApiAuth, type ApiCallFn, type ApiOptions, type ApiState, AuthMode, type Maybe } from '@proton/pass/types';
2 import type { ObjectHandler } from '@proton/pass/utils/object/handler';
3 import { getApiError } from '@proton/shared/lib/api/helpers/apiErrorHelper';
4 import { AppVersionBadError, InactiveSessionError } from '@proton/shared/lib/api/helpers/errors';
5 import { retryHandler } from '@proton/shared/lib/api/helpers/retryHandler';
6 import { OFFLINE_RETRY_ATTEMPTS_MAX, OFFLINE_RETRY_DELAY, RETRY_ATTEMPTS_MAX } from '@proton/shared/lib/constants';
7 import { API_CUSTOM_ERROR_CODES, HTTP_ERROR_CODES } from '@proton/shared/lib/errors';
8 import { withAuthHeaders, withUIDHeaders } from '@proton/shared/lib/fetch/headers';
9 import { wait } from '@proton/shared/lib/helpers/promise';
11 import { LockedSessionError, PassErrorCode } from './errors';
12 import type { RefreshHandler } from './refresh';
14 type ApiHandlersOptions = {
15 state: ObjectHandler<ApiState>;
17 getAuth: () => Maybe<ApiAuth>;
18 refreshHandler: RefreshHandler;
21 /* Simplified version of withApiHandlers.js :
22 * - handles recursive session refresh
23 * - handles appBadVersion
24 * - handles basic offline error detection
25 * - FIXME: handle code 9001 errors with human verification */
26 export const withApiHandlers = ({ state, call, getAuth, refreshHandler }: ApiHandlersOptions): ApiCallFn => {
27 return (options: ApiOptions) => {
30 retriesOnOffline = OFFLINE_RETRY_ATTEMPTS_MAX,
31 retriesOnTimeout = OFFLINE_RETRY_ATTEMPTS_MAX,
34 const next = async (attempts: number, maxAttempts?: number): Promise<any> => {
35 if (state.get('appVersionBad')) throw AppVersionBadError();
37 /** Unauthenticated API calls should not be
38 * blocked by the current API error state. */
39 if (!options.unauthenticated) {
40 if (state.get('sessionInactive')) throw InactiveSessionError();
41 if (state.get('sessionLocked')) throw LockedSessionError();
45 /** Check if the request was queued and possibly aborted :
46 * throw an error early to prevent triggering the API call */
47 if (options.signal?.aborted) throw new Error('Aborted');
49 const config = (() => {
50 /** If the request was passed a custom UID - use it
51 * instead of the standard authentication store value */
52 const auth = options.auth ?? getAuth();
53 if (!auth) return options;
55 return auth.type === AuthMode.COOKIE
56 ? withUIDHeaders(auth.UID, options)
57 : withAuthHeaders(auth.UID, auth.AccessToken, options);
60 return await call(config);
61 } catch (error: any) {
62 const { status, name, response } = error;
63 const { code } = getApiError(error);
65 if (maxAttempts && attempts >= maxAttempts) throw error;
67 /* Inactive extension session : only throw a `LockedSessionError`
68 * when we have not reached the max amount of unlock tries. After
69 * 3 unsuccessful attempts - we will get a 401 */
70 if (code === PassErrorCode.SESSION_LOCKED && status === HTTP_ERROR_CODES.UNPROCESSABLE_ENTITY) {
71 throw LockedSessionError();
74 if (code === API_CUSTOM_ERROR_CODES.APP_VERSION_BAD) throw AppVersionBadError();
76 if (name === 'OfflineError') {
77 if (attempts > retriesOnOffline) throw error;
78 await wait(OFFLINE_RETRY_DELAY);
79 return next(attempts + 1, retriesOnOffline);
82 if (name === 'TimeoutError') {
83 if (attempts > retriesOnTimeout) throw error;
84 return next(attempts + 1, retriesOnTimeout);
87 if (status === HTTP_ERROR_CODES.TOO_MANY_REQUESTS && !ignoreHandler.includes(status)) {
88 await retryHandler(error);
89 return next(attempts + 1, RETRY_ATTEMPTS_MAX);
92 if (status === HTTP_ERROR_CODES.UNPROCESSABLE_ENTITY) {
93 /* Catch inactive session errors during cookie upgrade */
94 if (code === PassErrorCode.INVALID_COOKIES_REFRESH) throw InactiveSessionError();
97 if (status === HTTP_ERROR_CODES.UNAUTHORIZED && !ignoreHandler.includes(status)) {
98 if (code === PassErrorCode.SESSION_ERROR) throw InactiveSessionError();
101 state.set('refreshing', true);
102 await refreshHandler(response, options);
103 return await next(attempts + 1, RETRY_ATTEMPTS_MAX);
105 if (err.status >= 400 && err.status <= 499) throw InactiveSessionError();
108 state.set('refreshing', false);