How to execute an async fetch request and then retry last failed request?

前端 未结 2 1763
一生所求
一生所求 2020-12-23 14:52

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 una

相关标签:
2条回答
  • 2020-12-23 15:36

    Accepted answer is quite good but it wouldn't work with 2 or more concurrent requests. I've crafted the one below after testing different cases with my token renew workflow that fits my needs.

    It's necessary to set errorLink before authLink in link pipeline. client.ts

    import { ApolloClient, from, HttpLink } from '@apollo/client'
    
    import errorLink from './errorLink'
    import authLink from './authLink'
    import cache from './cache'
    
    const httpLink = new HttpLink({
      uri: process.env.REACT_APP_API_URL,
    })
    
    const apiClient = new ApolloClient({
      link: from([errorLink, authLink, httpLink]),
      cache,
      credentials: 'include',
    })
    
    export default apiClient
    

    Cache shared between 2 apollo client instances for setting user query when my renewal token is expired

    cache.ts

    import { InMemoryCache } from '@apollo/client'
    
    const cache = new InMemoryCache()
    
    export default cache
    

    authLink.ts

    import { ApolloLink } from '@apollo/client'
    
    type Headers = {
      authorization?: string
    }
    
    const authLink = new ApolloLink((operation, forward) => {
      const accessToken = localStorage.getItem('accessToken')
    
      operation.setContext(({ headers }: { headers: Headers }) => ({
        headers: {
          ...headers,
          authorization: accessToken,
        },
      }))
    
      return forward(operation)
    })
    
    export default authLink
    

    errorLink.ts

    import { ApolloClient, createHttpLink, fromPromise } from '@apollo/client'
    
    import { onError } from '@apollo/client/link/error'
    
    import { GET_CURRENT_USER } from 'queries'
    import { RENEW_TOKEN } from 'mutations'
    
    import cache from './cache'
    
    let isRefreshing = false
    let pendingRequests: Function[] = []
    
    const setIsRefreshing = (value: boolean) => {
      isRefreshing = value
    }
    
    const addPendingRequest = (pendingRequest: Function) => {
      pendingRequests.push(pendingRequest)
    }
    
    const renewTokenApiClient = new ApolloClient({
      link: createHttpLink({ uri: process.env.REACT_APP_API_URL }),
      cache,
      credentials: 'include',
    })
    
    const resolvePendingRequests = () => {
      pendingRequests.map((callback) => callback())
      pendingRequests = []
    }
    
    const getNewToken = async () => {
      const oldRenewalToken = localStorage.getItem('renewalToken')
    
      const {
        data: {
          renewToken: {
            session: { renewalToken, accessToken },
          },
        },
      } = await renewTokenApiClient.mutate({
        mutation: RENEW_TOKEN,
        variables: { input: { renewalToken: oldRenewalToken } },
      })!
    
      localStorage.setItem('renewalToken', renewalToken)
      localStorage.setItem('accessToken', accessToken)
    }
    
    const errorLink = onError(({ graphQLErrors, operation, forward }) => {
      if (graphQLErrors) {
        for (const err of graphQLErrors) {
          switch (err?.message) {
            case 'expired':
              if (!isRefreshing) {
                setIsRefreshing(true)
    
                return fromPromise(
                  getNewToken().catch(() => {
                    resolvePendingRequests()
                    setIsRefreshing(false)
    
                    localStorage.clear()
    
                    // Cache shared with main client instance
                    renewTokenApiClient!.writeQuery({
                      query: GET_CURRENT_USER,
                      data: { currentUser: null },
                    })
    
                    return forward(operation)
                  }),
                ).flatMap(() => {
                  resolvePendingRequests()
                  setIsRefreshing(false)
    
                  return forward(operation)
                })
              } else {
                return fromPromise(
                  new Promise((resolve) => {
                    addPendingRequest(() => resolve())
                  }),
                ).flatMap(() => {
                  return forward(operation)
                })
              }
          }
        }
      }
    })
    
    export default errorLink
    
    0 讨论(0)
  • 2020-12-23 15:39

    I'm refreshing the token this way (updated OP's):

    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 }) => {
          // User access token has expired
          if (graphQLErrors && 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);
                  });
              });
            }
          }
        })
      ])
    });
    
    0 讨论(0)
提交回复
热议问题