如何执行异步获取请求,然后重试上次失败的请求?

Mys*_*yos 13 apollo react-apollo apollo-client

Apollo链接提供错误处理程序 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;
                            })
                        )
                    }
                }
            }
        }
    }
}),
Run Code Online (Sandbox Code Playgroud)

会发生什么:

  1. 初始graphQL查询由于未授权而运行并失败
  2. onError函数的ApolloLink执行.
  3. 执行刷新令牌的承诺.
  4. onError功能ApolloLink再次被执行?
  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
  }
}
Run Code Online (Sandbox Code Playgroud)

解决方案应该允许我们:

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

小智 35

我用这种方式刷新令牌(这是更新你的代码):

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)
                })
            })
          }
        }
      }
    })
  ])
})
Run Code Online (Sandbox Code Playgroud)

  • 我正在尝试在打字稿项目中使用此解决方案,但出现错误 `Argument of type '({ graphQLErrors, networkError, operation, forward }: ErrorResponse) =&gt; void | Observable&lt;{}&gt;' 不能分配给类型为 'ErrorHandler' 的参数。`? (3认同)

Vad*_*sov 11

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

需要在链接管道errorLink之前设置authLinkclient.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
Run Code Online (Sandbox Code Playgroud)

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

cache.ts

import { InMemoryCache } from '@apollo/client'

const cache = new InMemoryCache()

export default cache
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

  • 这个解决方案更加完整和优雅。 (3认同)

bop*_*opa 7

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

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)
    }
  })
}
Run Code Online (Sandbox Code Playgroud)

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