here's my client.js code
import {
ApolloClient,
HttpLink,
InMemoryCache,
IntrospectionFragmentMatcher
} from 'apollo-client-preset';
import { onError } from 'apollo-link-error';
import { setContext } from 'apollo-link-context';
import fragmentTypes from './data/fragmentTypes';
import Storage from './storage';
import config from './config';
import store from './store';
import actions from './actions';
const httpLink = new HttpLink({
uri: `${config.apiUrl}/graphql`
});
let token;
const withToken = setContext(async (_, { headers }) => {
({ token } = (await Storage.getUserAsync()) || {});
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : null
}
};
});
const resetToken = onError(({ response, networkError }) => {
if (networkError && networkError.statusCode === 401) {
// remove cached token on 401 from the server
store.dispatch(actions.signOut());
networkError = undefined;
}
});
const authFlowLink = withToken.concat(resetToken);
const link = authFlowLink.concat(httpLink);
const cache = new InMemoryCache({
fragmentMatcher: new IntrospectionFragmentMatcher({
introspectionQueryResultData: fragmentTypes
})
});
export default new ApolloClient({
link,
cache
});
when I get a 401 from my server, apollo throws, how do I catch this error and safely redirect the user back to the login screen?
as you can see, I set the networkError to undefined, but that obviously doesn't work.
@lifeiscontent hmm there are a couple ways to do this. You can catch the error in the component that makes the query that throws but that may be hard to keep up with.
Or you could write a link that listens to the error
call and redirects it as next
call. Or we could accept a callback to onError
that would allow you to pass data back up the chain preventing the error from being called. This would be a great addition in my mind.
const resetToken = onError(({ response, networkError }, cb) => {
if (networkError && networkError.statusCode === 401) {
// remove cached token on 401 from the server
store.dispatch(actions.signOut());
// pass something back to apollo
cb({ data: response.data });
}
});
@jbaxleyiii yeah, I agree. currently I'm doing the following:
TLDR: I just removed the observable.error calls in onError
import { ApolloLink, Observable, Operation } from 'apollo-link';
import { GraphQLError, ExecutionResult } from 'graphql';
export const onCatch = errorHandler => {
return new ApolloLink((operation, forward) => {
return new Observable(observer => {
let subscription;
try {
subscription = forward(operation).subscribe({
next: result => {
if (result.errors) {
errorHandler({
graphQLErrors: result.errors,
response: result,
operation
});
}
observer.next(result);
},
error: error => {
errorHandler({
operation,
networkError: error,
//Network errors can return GraphQL errors on for example a 403
graphQLErrors: error.result && error.result.errors
});
},
complete: observer.complete.bind(observer)
});
} catch (error) {
errorHandler({ networkError: error, operation });
}
return () => {
if (subscription) subscription.unsubscribe();
};
});
});
};
export class CatchLink extends ApolloLink {
constructor(errorHandler) {
super();
this.link = onCatch(errorHandler);
}
request(operation, forward) {
return this.link.request(operation, forward);
}
}
and usage
const resetToken = onCatch(({ response, networkError }) => {
if (networkError && networkError.statusCode === 401) {
// remove cached token on 401 from the server
store.dispatch(actions.signOut());
}
});
Auth0 has 24hr tokens, so 401s happen regularly. I can quickly fetch a new token without user action, so it would be nice to be able to first catch, and then retry the operation once I have an updated token.
For similar requirement (recovery from an error) I do that in following way:
interface ResponseError {
statusCode: number;
bodyText: string;
}
// action which happens on relevant error
const recoveryLink = new RetryLink({
delay: {
initial: 0,
},
attempts: {
max: 2,
retryIf: (error: ResponseError, operation) => {
if (error.statusCode === 401) {
return new Promise((resolve, reject) => {
// your refresh query here
fetch(
API_URL,
[...]
).then(response => {
// do something with response
[...]
// retry original query
resolve(true);
}).catch(() => {
resolve(false);
})
});
}
return false;
}
}
});
// enrich with JWT or another stuff
const enrichLink = setContext((_, { headers }) => {
const token = ...
return {
headers: {
...headers,
'Authorization': token ? token : '',
}
}
});
const httpLink = createHttpLink({
uri: API_URL + '/graphql',
credentials: 'include',
});
export default new ApolloClient({
link: ApolloLink.from([
recoveryLink,
enrichLink,
httpLink
]),
cache: new InMemoryCache(),
});
Future Googlers: This article has moved:
https://medium.com/@lucasmcgartland/refreshing-token-based-authentication-with-apollo-client-2-0-7d45c20dc703
https://github.com/apollographql/apollo-link/issues/297#issuecomment-350488527
This is amazing, thanks so much. I honestly can't believe you can't access the response on "networkError", feels like such a basic feature or am I missing something?
For
const { data, loading, error } = useQuery(QUER);
if (error) console.log(error.networkError.result.errors);
To find the graphql errors in a react component - surprisingly difficult to figure this out. Feel like it should be documented in useQuery
Best way is to use apollo-link-error
for catching 401
onError: ({networkError, graphQLErrors, operation}) => {
// raw "response" set by apollo-link-http
const { response } = operation.getContext()
if (response.status === 401) {
logout()
}
}
Most helpful comment
Auth0 has 24hr tokens, so 401s happen regularly. I can quickly fetch a new token without user action, so it would be nice to be able to first catch, and then retry the operation once I have an updated token.