Apollo-link: Catch 401 errors

Created on 2 Dec 2017  ·  9Comments  ·  Source: apollographql/apollo-link

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.

enhancement

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.

All 9 comments

@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()
    }
}
Was this page helpful?
0 / 5 - 0 ratings