How to execute an async fetch request and then ret

2020-02-17 09:50发布

问题:

Apollo link offers an error handler onError

Issue: Currently, we wish to refresh oauth tokens when they expires during an apollo call and we are unable to execute an async fetch request inside the onError properly.

Code:

initApolloClient.js

import { ApolloClient } from 'apollo-client';
import { onError } from 'apollo-link-error';
import { ApolloLink, fromPromise } from 'apollo-link';

//Define Http link
const httpLink = new createHttpLink({
    uri: '/my-graphql-endpoint',
    credentials: 'include'
});

//Add on error handler for apollo link

return new ApolloClient({
    link: ApolloLink.from([
        onError(({ graphQLErrors, networkError, operation, forward  }) => {
            if (graphQLErrors) {
                //User access token has expired
                if(graphQLErrors[0].message==="Unauthorized") {
                    //We assume we have both tokens needed to run the async request
                    if(refreshToken && clientToken) {
                        //let's refresh token through async request
                        return fromPromise(
                            authAPI.requestRefreshToken(refreshToken,clientToken)
                            .then((refreshResponse) => {
                                let headers = {
                                    //readd old headers
                                    ...operation.getContext().headers,
                                    //switch out old access token for new one
                                    authorization: `Bearer ${refreshResponse.access_token}`,
                                };

                                operation.setContext({
                                    headers
                                });

                                //Retry last failed request
                                return forward(operation);
                            })
                            .catch(function (error) {
                                //No refresh or client token available, we force user to login
                                return error;
                            })
                        )
                    }
                }
            }
        }
    }
}),

What happens is:

  1. Initial graphQL query runs and fails due to unauthorization
  2. The onError function of ApolloLink is executed.
  3. The promise to refresh the token is executed.
  4. The onError function of ApolloLink is executed again??
  5. The promise to refresh the token is completed.
  6. The initial graphQL query result is returned and its data is undefined

Between step 5 and 6, apollo doesn't re-run the initial failed graphQL query and hence the result is undefined.

Errors from console:

Uncaught (in promise) Error: Network error: Error writing result to store for query:
 query UserProfile($id: ID!) {
  UserProfile(id: $id) {
    id
    email
    first_name
    last_name
    }
    __typename
  }
}

The solution should allow us to:

  1. Run an async request when an operation fails
  2. Wait for the result of the request
  3. Retry failed operation with data from the request's result
  4. Operation should succeed to return its intended result

回答1:

I'm refreshing the token this way (this is updated your code):

import { ApolloClient } from 'apollo-client';
import { onError } from 'apollo-link-error';
import { ApolloLink, Observable } from 'apollo-link'; // <-- Add Observable

// Define Http link
const httpLink = new createHttpLink({
  uri: '/my-graphql-endpoint',
  credentials: 'include'
});

// Add on error handler for apollo link

return new ApolloClient({
  link: ApolloLink.from([
    onError(({ graphQLErrors, networkError, operation, forward }) => {
      if (graphQLErrors) {
        // User access token has expired
        if(graphQLErrors[0].message === "Unauthorized") {
          // We assume we have both tokens needed to run the async request
          if(refreshToken && clientToken) {
            // Let's refresh token through async request
            return new Observable(observer => {
              authAPI.requestRefreshToken(refreshToken, clientToken)
                .then(refreshResponse => {
                  operation.setContext(({ headers = {} }) => ({
                    headers: {
                      // Re-add old headers
                      ...headers,
                      // Switch out old access token for new one
                      authorization: `Bearer ${refreshResponse.access_token}` || null,
                    } 
                  }))
                })
                .then(() => {
                  const subscriber = {
                    next: observer.next.bind(observer),
                    error: observer.error.bind(observer),
                    complete: observer.complete.bind(observer)
                  }

                  // Retry last failed request
                  forward(operation).subscribe(subscriber)
                })
                .catch(error => {
                  // No refresh or client token available, we force user to login
                  observer.error(error)
                })
            })
          }
        }
      }
    })
  ])
})