If your GraphQL api needs token refresh option, you can pass custom fetch function for Apollo Client.
export const createApolloClient = ( url: string, logout: () => void, getAuthorizationData: () => { authorization: string }, refreshToken: () => Promise< { accessToken: string; refreshToken: string } | undefined >,) => new ApolloClientBase({ // ...other options link: ApolloLink.from([ // ...other options setContext(async (_, { headers }) => { return { headers: { ...headers, ...getAuthorizationData(), }, }; }), new HttpLink({ uri: url, fetch: fetchWithTokenRefresh(logout, refreshToken), }), ]), });
Custom fetch function for this request. You should tune hasUnauthorizedError
and
isRefreshRequestOptions
to match your api.
/** Global singleton for refreshing promise */let refreshingPromise: Promise<string> | null = null;/** Checks if GraphQl errors has unauthenticated error */const hasUnauthorizedError = (errors: Array<{ code?: ErrorCode }>): => Array.isArray(errors) && errors.some=> { return error.status === 401; // Distinguish unauthorized error here });/** Detects if customFetch is sending refresh request */const isRefreshRequestOptions = (options: RequestInit) => { try { const body = string); return body.operationName === 'RefreshToken'; } catch (e) { return false; }};/** fetchWithTokenRefresh is a custom fetch function with token refresh for apollo */export const fetchWithTokenRefresh = ( logout: () => void, refreshToken: () => { accessToken: string; refreshToken: string } | undefined >, ) => async (uri: string, options: RequestInit): Promise<Response> => { // already refreshing token, wait for it and then use refreshed token // or use empty authorization if refreshing failed if ( !isRefreshRequestOptions(options) && refreshingPromise && (options.headers as Record<string, string>)?.authorization ) { const newAccessToken = await refreshingPromise .catch(() => { // refreshing token from other request failed, retry without authorization return ''; }); options.headers = { ...(options.headers || {}), authorization: newAccessToken, }; } return fetch(uri, options).then(async const text = await response.text(); const json = // check for unauthorized errors, if not present, just return result if ( isRefreshRequestOptions(options) || !json?.errors || !hasUnauthorizedError(json.errors) ) { return { ...response, ok: true, json: async () => new Promise<unknown>(resolve => { resolve(json); }), text: async () => new Promise<string>(resolve => { resolve(text); }), }; } // If unauthorized, refresh token and try again if (!refreshingPromise) { refreshingPromise = refreshToken() .then(async (tokens): Promise<string> => { refreshingPromise = null; if (!tokens?.accessToken) { throw new Error('Session expired'); } return tokens?.accessToken; }) .catch(() => { refreshingPromise = null; // can't refresh token. logging out logout(); throw new Error('Session expired'); }); } // success or any non-auth error return refreshingPromise .then(async (newAccessToken: string) => { // wait for other request's refreshing query to finish, when retry return fetch(uri, { ...options, headers: { ...(options.headers || {}), authorization: newAccessToken, }, }); }) .catch(async () => { // refreshing token from other request failed, retry without authorization return fetch(uri, { ...options, headers: { ...(options.headers || {}), authorization: '', }, }); }); }); };