I was implementing OAuth refresh token exchange in my app and came across this misguided example in the Apollo docs:
onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
switch (err.extensions.code) {
// Apollo Server sets code to UNAUTHENTICATED
// when an AuthenticationError is thrown in a resolver
case "UNAUTHENTICATED":
// Modify the operation context with a new token
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
// Retry the request, returning the new observable
return forward(operation);
}
}
}
// To retry on network errors, we recommend the RetryLink
// instead of the onError link. This just logs the error.
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
});
As far as I understand from the GraphQL over HTTP draft spec, the server should be responding with 401 if it requires authentication, e.g. because a token expired, which must be handled as a network error. It would be wrong to return graphql field errors when authentication is required; resolvers should only throw authorization errors (meaning user is authenticated but not authorized to query a certain field or perform a certain mutation, while others may be allowed).
This example led me astray and I wasted time trying to handle token refresh in onError
. The proper way appears to be to use a RetryLink
with an async retryIf
function that exchanges a refresh token on 401, then resolves to true. I can’t find any examples of async retryIf
usage but I see in the code that it works and that’s what I had to do to get my refresh token exchange working without any errors popping up in the UI.