mirror of
https://github.com/muerwre/muerwre.github.io.git
synced 2025-04-25 02:46:39 +07:00
4.4 KiB
4.4 KiB
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 }>): boolean =>
Array.isArray(errors) &&
errors.some(error => {
return error.status === 401; // Distinguish unauthorized error here
});
/** Detects if customFetch is sending refresh request */
const isRefreshRequestOptions = (options: RequestInit) => {
try {
const body = JSON.parse(options?.body as 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: () => Promise<
{ 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 response => {
const text = await response.text();
const json = JSON.parse(text);
// check for unauthorized errors, if not present, just return result
if (
isRefreshRequestOptions(options) ||
!json?.errors ||
!Array.isArray(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: '',
},
});
});
});
};