问题描述
apollo link提供错误处理程序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; }) ) } } } } } }),
会发生什么:
- 初始graphql查询因未授权而导致的运行和失败
- ApolloLink的onError函数被执行.
- 执行刷新令牌的承诺.
- ApolloLink的函数再次执行??
- 刷新令牌的承诺完成.
- 返回初始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 } }
解决方案应允许我们:
- 在操作失败时运行异步请求 等待请求的结果
- 重试失败的操作与请求结果 的数据
- 操作应该成功返回其预期结果
推荐答案
我正在刷新以这种方式令牌(更新的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); }); }); } } }) ]) });
其他推荐答案
已接受答案非常好,但它无法使用2个或多个并发请求.在使用我的令牌续订工作流程测试不同的案例后,我在下面制作了下面的案例.
必须在链接流水线中设置authLink之前设置errorLink. 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
其他推荐答案
我们刚刚拥有同样的问题,经过一个非常复杂的解决方案,通过许多videables,我们使用的是一个简单的解决方案,使用将作为可观察到的可观察到的.
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) } }) }
所有挂起的请求都在等待令牌重定序列,然后将被转发.
问题描述
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 unable to execute an async fetch request inside the onError properly.
Code:
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; }) ) } } } } } }),
What happens is:
- Initial graphQL query runs and fails due to unauthorization
- The onError function of ApolloLink is executed.
- The promise to refresh the token is executed.
- The onError function of ApolloLink is executed again??
- The promise to refresh the token is completed.
- The initial graphQL query result is returned and its data is undefined
Between step 5 and 6, apollo doesn't re-run the initial failed graphQL query and hence the result is undefined.
Errors from console:
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 } }
The solution should allow us to:
- Run an async request when an operation fails
- Wait for the result of the request
- Retry failed operation with data from the request's result
- Operation should succeed to return its intended result
推荐答案
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); }); }); } } }) ]) });
其他推荐答案
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
其他推荐答案
We just had the same issues and after a very complicated solution with lots of Observeables we got a simple solution using promises which will be wrapped as an Observable in the end.
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) } }) }
All pending requests are waiting for the tokenRefreshPromise and will then be forwarded.