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;
})
)
}
}
}
}
}
}),
发生的事情是:
onError
的ApolloLink
功能。onError
的ApolloLink
函数又执行了??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):
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 个或更多并发请求。在使用符合我的需求的令牌更新工作流程测试不同的情况后,我制作了下面的一个。
在链接管道中,需要在
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
我们刚刚遇到了同样的问题,在使用大量 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,然后将被转发。
这对我来说效果很好
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,
})
对于 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);
});