首页 > 解决方案 > 刷新访问令牌并调用转发(操作)后不重新执行查询

问题描述

我觉得我的知识可能只是一个根本性的差距,因为我是 Apollo Client 的新手。但我已经仔细阅读了 Stack Overflow、GitHub 问题和 Google,以找到针对我遇到的问题的明显解决方案,但没有找到任何解决方案。

基本上我有以下 Apollo 客户端设置(简化):

const auth = new Auth()
const authMiddleware = new ApolloLink((operation, forward) => {
  const authToken = auth.getToken().access_token

  console.log(authToken)

  operation.setContext(({ headers = {} }) => ({
    headers: {
      ...headers,
      authorization: authToken ? `Bearer ${authToken}` : ''
    }
  }))

  return forward(operation)
})
const cache = new InMemoryCache()
const errorLink = onError(({ forward, graphQLErrors, networkError, operation }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ extensions, locations, message, path }) => {
      if (extensions.code === 'access-denied') {
        auth.refresh()
          .then(() => {
            console.log(`new access token: ${auth.getToken().access_token}`)
            return forward(operation)
          }).catch((error) => {
            handleLogout(error)
          })
      }
    })
  }
})
const handleLogout = (reason) => {
  auth.logout()
}
const httpLink = new HttpLink({ uri: '' })

const client = new ApolloClient({
  cache: cache,
  link: ApolloLink.from([
    errorLink,
    authMiddleware,
    httpLink
  ])
})

我有一个简单的查询:

client.query({
  query: Queries.MyQuery
}).then((response) => {
  console.log(response)
}, (error) => {
  console.log(error)
})

如果客户端第一次运行时存在有效的 OAuth 访问令牌,则客户端成功执行查询。但是,如果我使 OAuth 服务器上的访问令牌过期,然后尝试执行查询,则它不会成功完成。

调试时,我可以看到发生了什么:

  1. authMiddleware将旧访问令牌正确添加到请求标头。
  2. 请求失败,因为令牌不再有效。这是由 处理的财产errorLink
  3. errorLink还成功检索到新的访问令牌并返回forward(operation).
  4. authMiddleware再次调用,添加新的访问令牌,然后返回forward(operation).

这就是事情崩溃的地方。查询永远不会重新执行。如果我手动刷新页面以重新执行查询,它将使用新的访问令牌并成功完成。

通过阅读文档,听起来我设置它的方式应该可以工作,但显然我做错了。

标签: reactjsapollo-client

解决方案


通过挖掘各种来源,我能够拼凑出正在发生的事情。这很令人困惑,主要是因为过去有很多开发人员都在为此苦苦挣扎(而且似乎仍然如此),所以那里有大量过时的解决方案和帖子。

这个 GitHub 问题是最有用的信息来源,尽管它附加到现在已弃用的存储库。这个 Stack Overflow 的答案也很有帮助。

我花了一些时间使用实用方法将 Promise 转换为 Observable,但如果您使用fromPromise.

这是我最终得到的适用于 Apollo Client 3.2.0 的解决方案:

const authLink = new ApolloLink((operation, forward) => {
  const authToken = auth.getToken().access_token

  console.info(`access token: ${authToken}`)
  operation.setContext(({ headers }) => ({
    headers: {
      ...headers,
      authorization: authToken ? `Bearer ${authToken}` : ''
    }
  }))

  return forward(operation)
})
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors) {
    const firstGraphQLError = graphQLErrors[0]

    if (firstGraphQLError.extensions.code === 'access-denied') {
      let innerForward

      if (!isRefreshing) {
        isRefreshing = true
        innerForward = fromPromise(
          auth.refresh()
            .then(() => {
              const authToken = auth.getToken().access_token
              console.info(`access token refreshed: ${authToken}`)
              resolvePendingRequests()
              return authToken
            })
            .catch(() => {
              pendingRequests = []
              // Log the user out here.
              return false
            })
            .finally(() => {
              isRefreshing = false
            })
        ).filter(value => Boolean(value))
      } else {
        innerForward = fromPromise(
          new Promise(resolve => {
            pendingRequests.push(() => resolve())
          })
        )
      }

      return innerForward.flatMap(() => {
        return forward(operation)
      })
    } else {
      console.log(`[GraphQL error]: Message: ${firstGraphQLError.message}, Location: ${firstGraphQLError.locations}, Path: ${firstGraphQLError.path}`)
    }
  }

  if (networkError) {
    console.log(`[Network error]: ${networkError}`)
  }
})

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: from([
    errorLink,
    authLink,
    new HttpLink({ uri: '' })
  ])
})

该解决方案还处理多个并发请求,将它们排队并在访问令牌被刷新后请求它们。


推荐阅读