Open jerelmiller opened 1 year ago
@jerelmiller, this is the exact issue i'm facing right now. Could you point me to a couple examples of issues users have fixed and how it was resolved? Thanks!
Hey @arahman4710 👋
I'd recommend taking a look at the implementation of the setContext
link. The trick here is to return an Observable
from the ApolloLink
that waits to call forward(operation)
until the promise has resolved. Hope this helps!
const errorLink = onError(({ networkError, forward, operation }) => {
operation.setContext(async ({ headers = {} }) => {
const token = await getNewToken();
if (token) {
return {
headers: {
...headers,
Authorization: token ? `Bearer ${token}` : "",
},
};
} else {
// logout
}
});
return forward(operation);
});
Hey @usmansbk 👋
Appreciate the code snippet, but I'm afraid there are a few things that won't work with that implementation.
operation.setContext
expects the value returned from the function to be a synchronous value, which is then spread onto the existing context. Using an async
function here means that a promise will be returned, so you'd be spreading the promise properties instead of the new context object.
Additionally, even if this were to work correctly, forward(operation)
is going to execute first, which means the async
function passed to setContext
would execute too late in the process and the request would start before the async function could be resolved.
What I'm specifically looking for in this issue is to both document how to implement a custom link yourself that needs to use async code inside its request handler function and to better document how you might use async data within onError
since an async
function here won't work correctly. In other words, how people can do this kind of thing:
const customLink = new ApolloLink((operation, forward) => {
// do some async stuff in here
})
Just wanted to make sure that you were aware that the snippet above won't execute as you'd expect!
Hey @usmansbk 👋
Appreciate the code snippet, but I'm afraid there are a few things that won't work with that implementation.
operation.setContext
expects the value returned from the function to be a synchronous value, which is then spread onto the existing context. Using anasync
function here means that a promise will be returned, so you'd be spreading the promise properties instead of the new context object.Additionally, even if this were to work correctly,
forward(operation)
is going to execute first, which means theasync
function passed tosetContext
would execute too late in the process and the request would start before the async function could be resolved.What I'm specifically looking for in this issue is to both document how to implement a custom link yourself that needs to use async code inside its request handler function and to better document how you might use async data within
onError
since anasync
function here won't work correctly. In other words, how people can do this kind of thing:const customLink = new ApolloLink((operation, forward) => { // do some async stuff in here })
Just wanted to make sure that you were aware that the snippet above won't execute as you'd expect!
It works perfectly for me. 🤔 But thanks for the clarification. I will research more and see.
@usmansbk interesting! I would think it would have some problems, but now I'm curious to try it out myself!
You should. This is the complete snippet on how I implemented refreshTokenLink.
const retryLink = new RetryLink();
const httpLink = new HttpLink({
uri: env.graphqlApiEndpoint,
});
const refreshTokenLink = onError(({ networkError, forward, operation }) => {
if (networkError) {
const error = networkError as ServerNetworkError;
if (error.statusCode === 401) {
const authState = cache.readQuery({
query: AuthState,
});
if (authState?.auth) {
const { accessToken, refreshToken } = authState.auth;
if (accessToken && refreshToken) {
operation.setContext(async () => {
const response = await fetch(
`${env.restApiEndpoint}/auth/token/refresh`,
{
method: "POST",
headers: {
access_token: accessToken,
refresh_token: refreshToken,
},
}
);
if (response.status === 200) {
const { accessToken, refreshToken } = (await response.json()) as {
accessToken: string;
refreshToken: string;
};
if (accessToken && refreshToken) {
cache.writeQuery({
query: AuthState,
data: {
auth: {
accessToken,
refreshToken,
},
},
});
}
} else if (response.status === 401) {
cache.writeQuery({
query: AuthState,
data: {
auth: null,
},
});
}
});
return forward(operation);
}
}
}
}
});
const authLink = setContext((_, { headers }) => {
const authState = cache.readQuery({
query: AuthState,
});
const token = authState?.auth?.accessToken;
return {
headers: {
...headers,
Authorization: token ? `Bearer ${token}` : "",
},
};
});
const client = new ApolloClient({
cache,
link: from([retryLink, refreshTokenLink, authLink, httpLink]),
});
You should. This is the complete snippet on how I implemented refreshTokenLink.
const retryLink = new RetryLink(); const httpLink = new HttpLink({ uri: env.graphqlApiEndpoint, }); const refreshTokenLink = onError(({ networkError, forward, operation }) => { if (networkError) { const error = networkError as ServerNetworkError; if (error.statusCode === 401) { const authState = cache.readQuery({ query: AuthState, }); if (authState?.auth) { const { accessToken, refreshToken } = authState.auth; if (accessToken && refreshToken) { operation.setContext(async () => { const response = await fetch( `${env.restApiEndpoint}/auth/token/refresh`, { method: "POST", headers: { access_token: accessToken, refresh_token: refreshToken, }, } ); if (response.status === 200) { const { accessToken, refreshToken } = (await response.json()) as { accessToken: string; refreshToken: string; }; if (accessToken && refreshToken) { cache.writeQuery({ query: AuthState, data: { auth: { accessToken, refreshToken, }, }, }); } } else if (response.status === 401) { cache.writeQuery({ query: AuthState, data: { auth: null, }, }); } }); return forward(operation); } } } } }); const authLink = setContext((_, { headers }) => { const authState = cache.readQuery({ query: AuthState, }); const token = authState?.auth?.accessToken; return { headers: { ...headers, Authorization: token ? `Bearer ${token}` : "", }, }; }); const client = new ApolloClient({ cache, link: from([retryLink, refreshTokenLink, authLink, httpLink]), });
I have an implementation to refresh the jwt token where I am processing graphQLErrors instead of networkError and my implementation is breaking(stop refreshing jwt) randomly in prod. My code is attached, any insights?
const errorLink = onError(({ graphQLErrors, operation, forward }): any => {
if (graphQLErrors && graphQLErrors.length > 0) {
for (const error of graphQLErrors) {
if (error.extensions && error.extensions.code && error.extensions.code === 'UNAUTHENTICATED') {
let forward$;
if (!isRefreshing) {
isRefreshing = true;
forward$ = fromPromise(
getNewToken()
.then((accessToken) => {
// Store the new tokens for your auth link
resolvePendingRequests();
isRefreshing = false;
return accessToken;
})
.catch((error) => {
// Handle token refresh errors e.g clear stored tokens, redirect to login, ...
logout(error);
return;
}),
).filter((value) => Boolean(value));
} else {
// Will only emit once the Promise is resolved
forward$ = fromPromise(
new Promise((resolve) => {
pendingRequests.push(() => resolve(true));
}),
);
}
return forward$.flatMap(() => forward(operation));
} else if (error.message) {
console.log(`[GraphQL error]: Message: ${error.message} - Operation: ${operation.operationName}`);
}
}
}
});
Hey @FaizanMostafa 👋
At a glance your code seems fine, but without being able to run this or use it in context with the other links in your link chain, its difficult to tell what might be wrong.
I'd suggest reaching out in our discord channel where others in the community might be able to provide some pointers on how they handle auth errors/refreshes.
We've seen a few cases where users struggle with async handling inside of Apollo Link. The link chain expects returned observables from the request handlers, but typically devs want to execute async code.
setContext
is one of the rare links that allow you to pass an async function and operate as expected. I've also seen devs try and return async functions fromonError
, but this will not work because that link expects that if you want to retry an operation, you will return an observable. This causes issues however because promises returned by an asynconError
won't correctly be handled by the retry functionality.We should update our documentation and provide a guide or troubleshooting tips on how to add async functions within an apollo link to help engineers who typically need this functionality.