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

问题描述 投票:0回答:5

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

发生的事情是:

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

解决方案应该允许我们:

  1. 操作失败时运行异步请求
  2. 等待请求结果
  3. 使用请求结果中的数据重试失败的操作
  4. 操作应该成功返回其预期结果
apollo react-apollo apollo-client
5个回答
75
投票

我正在以这种方式刷新令牌(更新了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);
              });
          });
        }
      }
    })
  ])
});

24
投票

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

在链接管道中,需要在

errorLink
之前设置
authLink
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

在 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

7
投票

我们刚刚遇到了同样的问题,在使用大量 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,然后将被转发。


1
投票

这对我来说效果很好

apollo-client.ts

const errorLink = onError(({ graphQLErrors, operation, forward }) => {
  if (graphQLErrors) {
    if (
      graphQLErrors.some(({ extensions: { errorType } }) =>
        ['UNAUTHENTICATED', 'PERMISSION_DENIED'].includes(errorType as string)
      )
    ) {
      return fromPromise(fetch('/api/refresh', { method: 'POST' })).flatMap(
        () => forward(operation)
      )
    }
  }
  return forward(operation)
})

const httpLink = new HttpLink({ uri: 'http://localhost:3000/api/graphql' })

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

0
投票

对于 Apollo 客户端 3,您可以简单地使用 setContext 进行异步操作:

const errorLink = onError(({ networkError, forward, operation }) => {
        operation.setContext(async ({ headers = {} }) => {
           const token = await getNewToken();
       
            return {
              headers: {
                ...headers,
                Authorization: token ? `Bearer ${token}` : "",
              },
            };
          } else {
             // logout
          }
        });

        return forward(operation);
});
最新问题
© www.soinside.com 2019 - 2024. All rights reserved.