【问题标题】:How to execute an async fetch request and then retry last failed request?如何执行异步获取请求,然后重试上次失败的请求?
【发布时间】:2018-11-30 14:21:18
【问题描述】:

Apollo link offers an error handler onError

问题: 目前,我们希望在 apollo 调用期间到期时刷新 oauth 令牌,并且我们无法在 onError 中正确执行异步获取请求。

代码:

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;
                            })
                        )
                    }
                }
            }
        }
    }
}),

会发生什么:

  1. 初始 graphQL 查询运行并由于未经授权而失败
  2. ApolloLinkonError 函数被执行。
  3. 刷新令牌的承诺已执行。
  4. ApolloLinkonError函数又执行了??
  5. 刷新令牌的承诺已完成。
  6. 返回初始graphQL查询结果,数据为undefined

在第 5 步和第 6 步之间,apollo 不会重新运行初始失败的 graphQL 查询,因此结果是 undefined

来自控制台的错误:

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
  }
}

解决方案应该允许我们:

  1. 在操作失败时运行异步请求
  2. 等待请求结果
  3. 使用请求结果中的数据重试失败的操作
  4. 操作应成功返回其预期结果

【问题讨论】:

  • 你能给我看看 authAPI.refreshToken() 的代码吗?
  • @MinhKha authAPI.refreshToken() 返回一个 axios 承诺,它调用身份验证端点来刷新令牌。

标签: apollo react-apollo apollo-client


【解决方案1】:

我们只是遇到了同样的问题,在一个包含大量 Observable 的非常复杂的解决方案之后,我们得到了一个使用 Promise 的简单解决方案,最终将被包装为一个 Observable。

let tokenRefreshPromise: Promise = Promise.resolve()
let isRefreshing: boolean

function createErrorLink (store): ApolloLink {
  return onError(({ graphQLErrors, networkError, operation, forward }) => {
    if (graphQLErrors) {
      // this is a helper method where we are checking the error message
      if (isExpiredLogin(graphQLErrors) && !isRefreshing) {
        isRefreshing = true
        tokenRefreshPromise = store.dispatch('authentication/refreshToken')
        tokenRefreshPromise.then(() => isRefreshing = false)
      }
      return fromPromise(tokenRefreshPromise).flatMap(() => forward(operation))
    }
    if (networkError) {
      handleNetworkError(displayErrorMessage)
    }
  })
}

所有待处理的请求都在等待 tokenRefreshPromise,然后将被转发。

【讨论】:

    【解决方案2】:

    已接受的答案非常好,但不适用于 2 个或更多并发请求。在使用符合我需要的令牌更新工作流程测试了不同的案例后,我制作了下面的一个。

    在链接管道中必须在authLink 之前设置errorLinkclient.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
    

    2 个 apollo 客户端实例之间共享缓存,用于在我的续订令牌过期时设置用户查询

    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
    

    【讨论】:

    • 这个方案更加完善和优雅。
    • 我可能遗漏了一些东西,但是如果getNewToken() 失败会发生什么? Catch 块不会重新抛出错误,我们最终会在 flatMap 中返回转发操作。流程似乎是 someHttpRequestWhichRequiresAuth -> 401 -> getNewToken -> 4xx 或 5xx -> someHttpRequestWhichRequiresAuth(再次)-> 401 如果我们无法刷新,似乎没有必要重新运行 someHttpRequestWhichRequiresAuth令牌。
    • 如果 getNewToken 由于某种原因失败,我只需注销(清除 localStorage 并获取 currentUser: null。在 99% 的情况下,这是因为 renewToken 已过期。对我来说已经足够了。
    【解决方案3】:

    我正在以这种方式刷新令牌(更新的 OP):

    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);
                  });
              });
            }
          }
        })
      ])
    });
    

    【讨论】:

    • 我正在尝试在 typescript 项目中使用此解决方案,但收到错误 Argument of type '({ graphQLErrors, networkError, operation, forward }: ErrorResponse) => void | Observable<{}>' is not assignable to parameter of type 'ErrorHandler'.?
    • observer in return new Observable(observer 未定义
    猜你喜欢
    • 1970-01-01
    • 2018-02-20
    • 1970-01-01
    • 2023-01-25
    • 1970-01-01
    • 2020-03-22
    • 1970-01-01
    • 2022-11-21
    • 1970-01-01
    相关资源
    最近更新 更多