问题标签
问题
这里提到了我试图完成的精确场景:
https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-error#retrying -failed-requests
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
但是,如果令牌已过期,则应调用异步resfreshToken
并“等待”,然后getNewToken
才能返回有效的身份验证令牌。 我认为。
我的问题是,如何进行异步resfreshToken
调用。 我已经尝试过await refreshToken()
(它在完成时解决一个承诺),但是从我记录的堆栈跟踪来看,这似乎与 RxJS 混淆了很多。 我是 RxJS n00b,非常感谢任何帮助!
如果你对promise比较熟悉,可以使用fromPromise
helper
import { fromPromise } from 'apollo-link';
return fromPromise(refreshToken().then(token => {
operation.setContext({
headers: {
...oldHeaders,
authorization: token,
},
});
return forward(operation);
}))
@thymikee尝试了您的解决方案,但失败并显示以下消息:
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
activated
created_at
updated_at
last_active
roles {
id
name
__typename
}
permissions {
name
value
__typename
}
profile {
address
secondary_email
phone {
id
number
type {
id
name
__typename
}
__typename
}
__typename
}
__typename
}
}
Cannot read property 'UserProfile' of undefined
at new ApolloError (ApolloError.js:43)
at QueryManager.js:327
at QueryManager.js:759
at Array.forEach (<anonymous>)
at QueryManager.js:758
at Map.forEach (<anonymous>)
at QueryManager.webpackJsonp../node_modules/apollo-client/core/QueryManager.js.QueryManager.broadcastQueries (QueryManager.js:751)
at QueryManager.js:254
进一步检查发现,在使用上述代码时,Apollo 链接的onError
被调用了两次。 即使将refresh token
承诺限制为运行一次,也不能修复错误。
发生的事情是:
1) 执行初始查询
2) 它失败并运行 apollo 的链接onError
3) ?? 它再次运行 apollo 的链接onError
4) Promise to refresh token, 在onError
执行完毕并解决。
5)(承诺成功后不会第二次执行初始查询)
6) 初始查询将包含data
结果返回为未定义
这是希望有人找到解决方案,否则我们将不得不恢复使用长期访问令牌,而不是在到期时刷新它们。
如果您的令牌检索逻辑正确,则onError
应该只调用一次。 看起来您的令牌查询有问题
@thymikee使用虚拟承诺切换异步请求。 仍然失败并显示上述消息,并且初始查询未运行两次。 所有令牌在测试时均有效。
代码:
return fromPromise(
new Promise((resolve) => {
let headers = {
//readd old headers
...operation.getContext().headers,
//switch out old access token for new one
authorization: `Bearer mynewaccesstoken`,
};
operation.setContext({
headers
});
return resolve(forward(operation));
})
)
编辑:删除了fromPromise
并且它可以正常工作。 不知何故,链接堆栈的处理在返回结果之前结束,因此不执行forward(operation)
。
在分析fromPromise
代码和提交 #172 后, fromPromise
只能用于推测与 Apollo Link 对象。
在研究了一个解决方案后,我终于偶然发现了这个项目: apollo-link-token-refresh
我的阿波罗链接堆栈现在如下:
[
refreshTokenLink,
requestLink,
batchHttpLink
]
在执行对 graphql 端点的任何响应之前,始终调用refreshTokenLink
来检查访问令牌,并且它的工作原理就像一个魅力。
不幸的是,这假设对 graphql 端点的调用必须始终经过身份验证(在我的情况下就是如此)。
看起来onError
回调不接受aync
函数或Promise
返回。 见代码https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74
之前曾报告过此问题:#190
我认为如果apollo-link-error
可以处理Promise
会更好,类似于apollo-link-retry
在这里所做的:#436
有同样的问题,使用 apollo 和 react native 我需要从 AsyncStorage onError 中删除一些令牌,所以它需要是一个异步函数
这个解决方案对我有用: //stackoverflow.com/a/51321068/60223
我通过创建一个实用程序promiseToObservable.js
解决了这个问题:
import { Observable } from 'apollo-link';
export default promise =>
new Observable((subscriber) => {
promise.then(
(value) => {
if (subscriber.closed) return;
subscriber.next(value);
subscriber.complete();
},
err => subscriber.error(err)
);
return subscriber; // this line can removed, as per next comment
});
接着
import { onError } from 'apollo-link-error';
import promiseToObservable from './promiseToObservable';
export default (refreshToken: Function) =>
onError(({
forward,
graphQLErrors,
networkError = {},
operation,
// response,
}) => {
if (networkError.message === 'UNAUTHORIZED') { // or whatever you want to check
// note: await refreshToken, then call its link middleware again!
return promiseToObservable(refreshToken()).flatMap(() => forward(operation));
}
});
@crazy4groovy谢谢你的例子,它真的Observable
subscriber
是无效的返回值:它应该是ZenObservable.SubscriptionObserver
:
export declare type Subscriber<T> = ZenObservable.Subscriber<T>;
export declare const Observable: {
new <T>(subscriber: Subscriber<T>): Observable<T>;
};
export declare namespace ZenObservable {
interface SubscriptionObserver<T> {
closed: boolean;
next(value: T): void;
error(errorValue: any): void;
complete(): void;
}
type Subscriber<T> = (observer: SubscriptionObserver<T>) => void | (() => void) | Subscription;
}
即返回 undefined 是安全的。 我想它应该在项目的 README 文件中提到。
UPD:我添加了一个关于这个的 PR: https :
这个问题在谷歌上排名很高,所以我在这里分享我的解决方案来帮助一些人: https :
我已经尝试过你的解决方案,但我面临着新的问题:
Argument of type '(this: Observable<{}>, observer: Subscriber<{}>) => Observable<{}> | Promise<{}>' is not assignable to parameter of type '(this: Observable<{}>, subscriber: Subscriber<{}>) => TeardownLogic'.
Type 'Observable<{}> | Promise<{}>' is not assignable to type 'TeardownLogic'.
Type 'Observable<{}>' is not assignable to type 'TeardownLogic'.
Property 'unsubscribe' is missing in type 'Observable<{}>' but required in type 'Unsubscribable'
刷新后,你们如何存储新的身份验证令牌?
当然,我可以在重试请求中设置新的标头,但是原始访问令牌(我存储在 cookie 中)不会更新,这意味着对服务器的每个请求都将使用旧的访问令牌(随后将需要再次刷新)。
出于某种原因,每当我尝试在刷新期间更新 cookie 时,都会收到以下错误消息(我在这里创建了一个新问题):
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (_http_outgoing.js:470:11)
at setCookie (/root/SimplyTidyAdmin/node_modules/nookies/dist/index.js:98:17)
at /root/SimplyTidyAdmin/.next/server/static/CAhshxrRWHVF6Gzbce~pU/pages/_app.js:1273:63
at process._tickCallback (internal/process/next_tick.js:68:7)
@StupidSexyJake也许这会帮助你https://stackoverflow.com/questions/55356736/change-apollo-client-options-for-jwt-token我遇到了关于如何更新令牌的类似问题
嗨,谢谢@crazy4groovy。 我已经尝试了您的解决方案,但我仍然遇到问题,即在将新令牌设置为请求之前调用将令牌附加到 graphql 请求的中间件。 因此,标头仍然具有无效令牌。
一点背景信息:我们收到网络错误,当令牌无效时,通过刷新令牌,我们可以获得一个新令牌并重试。 但是由于中间件在收集刷新令牌并将其设置为本地存储之前被调用,因此它仍然具有无效的令牌。 刷新令牌逻辑工作正常,因为我们最终会获得新的令牌集。 我已经调试了这个问题,时间如下:
onError
通过promiseToObservable
逻辑处理并重试。onRefreshToken
承诺中获取令牌之前,中间件已经使用旧令牌进行了第二次运行。这是这些部分的片段(跳过 onRefreshtoken。它是一个异步函数,返回一个 Promise):
const promiseToObservable = (promise: Promise<any>) =>
new Observable((subscriber: any) => {
promise.then(
value => {
console.log(subscriber);
if (subscriber.closed) return;
subscriber.next(value);
subscriber.complete();
},
err => subscriber.error(err)
);
});
const authMiddleware = setContext((operation: GraphQLRequest) => {
const token = localStorage.getItem('ca_token');
return {
headers: {
...(token && !isSkipHeader(operation)
? { authorization: `Bearer ${token}` }
: {})
}
};
});
const errorLink = onError(
({
networkError,
graphQLErrors,
operation,
forward
}: ErrorResponse): any => {
if (networkError) {
switch (networkError.statusCode) {
case 401:
console.warn('Refreshing token and trying again');
// await refreshToken, then call its link middleware again
return promiseToObservable(onRefreshToken(client.mutate)).flatMap(() => forward(operation));
default:
// Handle all other errors here. Irrelevant here.
}
}
if (graphQLErrors) {
// Handle gql errors, irrelevant here.
}
}
);
你能告诉我我在这里遗漏了什么吗? 非常感谢提前...
好的,抱歉造成混乱,如果有的话......
我找到了答案,这是一个愚蠢的答案,在寻找了几个小时之后找到了 - 当然 - 在这里发帖之后:在 apollo 客户端的初始化期间,我交换了中间件和错误链接。 现在它起作用了。 错误链接应该是第一个,显然..
旧: link: from([authMiddleware, errorLink, /* others */])
新: link: from([errorLink, authMiddleware, /* others */])
再次抱歉..
大家好,
我在使用 onError 刷新令牌时遇到以下问题。 出于使用 nextjs 的 SSR 的目的,我正在从所有 graphql 查询中收集数据,但是例如,当我们有 2 个查询并且每个查询都以错误结束时会发生什么,因为 jwt 令牌已过期。 然后它触发两次 onError 并且我们调用两次刷新令牌,这是昂贵的。 我无法弄清楚问题可能来自哪里。 这是我正在使用的代码。 你能帮忙解决这个问题吗?
https://gist.github.com/shaxaaa/15817f1bcc7b479f3c541383d2e83650
我在这个问题上挣扎了一段时间,但我终于让它工作了。 我一起扔了一个包裹。
https://github.com/baleeds/apollo-link-refresh-token
该软件包与名为 apollo-link-token-refresh 的软件包之间的主要区别在于,该软件包将在尝试刷新之前等待网络错误。
如果你们有改变的想法,请告诉我。
下面是基本用法:
const refreshTokenLink = getRefreshTokenLink({
authorizationHeaderKey: 'Authorization',
fetchNewAccessToken,
getAccessToken: () => localStorage.getItem('access_token'),
getRefreshToken: () => localStorage.getItem('refresh_token'),
isAccessTokenValid: accessToken => isTokenValid(accessToken),
isUnauthenticatedError: graphQLError => {
const { extensions } = graphQLError;
if (
extensions &&
extensions.code &&
extensions.code === 'UNAUTHENTICATED'
) {
return true;
}
return false;
},
});
我通过创建一个实用程序
promiseToObservable.js
解决了这个问题:import { Observable } from 'apollo-link'; export default promise => new Observable((subscriber) => { promise.then( (value) => { if (subscriber.closed) return; subscriber.next(value); subscriber.complete(); }, err => subscriber.error(err) ); return subscriber; // this line can removed, as per next comment });
接着
import { onError } from 'apollo-link-error'; import promiseToObservable from './promiseToObservable'; export default (refreshToken: Function) => onError(({ forward, graphQLErrors, networkError = {}, operation, // response, }) => { if (networkError.message === 'UNAUTHORIZED') { // or whatever you want to check // note: await refreshToken, then call its link middleware again! return promiseToObservable(refreshToken()).flatMap(() => forward(operation)); } });
我使用它并发现它在刷新令牌请求后仍然使用旧令牌。 所以,我尝试如下:
return promiseToObservable(refreshToken()).flatMap((value) => {
operation.setContext(({ headers = {} }) => ({
headers: {
// re-add old headers
// ...headers,
Authorization: `JWT ${value.token}`
}
}));
return forward(operation)
});
它有效。
但是,它仍然存在一个问题,如果我添加...headers
(意味着重新添加旧标头),则在发送转发请求之前会出现问题:
ERROR Error: Network error: Cannot read property 'length' of null
我认为 ...headers 中的授权可能与新的授权冲突。
上面的问题是在 apollo-angular "apollo-angular-link-http": "^1.6.0",
而不是 apollo-client "apollo-link-http": "^1.5.16",
而链接错误是相同的"apollo-link-error": "^1.1.12",
另一种语法:眼睛:
import Vue from 'vue'
import { Observable } from 'apollo-link'
import { onError } from 'apollo-link-error'
const onGraphqlError = async ({ graphQLErrors = [], observer, operation, forward }) => {
// here you could call the refresh query in case you receive an expired error
for (let error of graphQLErrors)
observer.next(forward(operation)) // this line would retry the operation
}
const onNetworkError = async ({ observer, networkError, operation, forward }) => { }
export const errorHandler = opt => new Observable(async observer => {
try {
const payload = { ...opt, observer }
await Promise.all([onGraphqlError(payload), onNetworkError(payload)])
if (observer.closed) return
observer.complete()
} catch (error) {
observer.error(error)
}
})
你好! 我使用完整的 websocket 传输,需要请求令牌查询。 不知道该怎么做。
当服务器响应accessToken
已过期时,我想做一个接收请求。
import { onError } from "apollo-link-error";
import gql from 'graphql-tag'
// Client: VUE APOLLO
const q = {
query: gql`query token { token { accessToken } }`,
manual: true,
result({ data, loading }) {
if (!loading) {
console.log(data)
}
},
}
const link = onError(({ graphQLErrors, networkError, operation, response, forward }) => {
if (networkError) {
switch (networkError.message) {
case 'accessTokenExpired':
console.log('accessTokenExpired')
return forward(q) // NOT WORKS, NEED HELP
case 'unauthorized':
return console.log('unauthorized')
default:
return forward(operation)
}
}
return forward(operation)
})
export default link
@nikitamarcius我们在上面发布了解决方法,看看 observables
我无法更新令牌任何人都可以提供工作示例
@Ramyapriya24这是我正在使用的代码。
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/link-context';
import AuthService from 'services/auth-service' // this is my implementation
const asyncAuthLink = setContext(async () => {
// this is an async call, it will be done before each request
const { token } = await AuthService.getCredentials();
return {
headers: {
authorization: token
},
};
},
);
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
});
export const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: asyncAuthLink.concat(httpLink),
});
@adrianolsk你能提供写的服务代码吗
import AuthService from 'services/auth-service' // 这是我的实现
const { 令牌 } = 等待 AuthService.getCredentials();
当我尝试导入服务时出现错误
那是我的服务,它只是从 react-native 读取 AsyncStorage,所以登录后我在那里设置了值,在每个请求之前,代码只是获取信息并设置在标题中,你可以这样做,或者如果你使用 localStorage在网上。
您将要使用的信息存储在哪里?
你可以用这个
//save the token after login or when it refreshes
localStorage.setItem('token', yourToken);
并使用它
const asyncAuthLink = setContext(() => {
// grab token from localStorage
const token = localStorage.getItem('token');
return {
headers: {
authorization: token
},
};
},
);
@adrianolsk感谢您的解释,但我使用的是 angular 我无法在 grapqh.module.ts 文件中导入服务 我在使用该服务时遇到错误
谁能知道如何在不使用类和构造函数的情况下使用 module.ts 文件中的服务
谢谢
我正在尝试使用fromPromise
异步刷新令牌。
基本上遵循这篇文章的第三个框
我成功地获取和存储了令牌,但是catch
或filter
或flatMap
都没有被调用。 我不确定如何调试它,所以一些建议会有所帮助。
if (token && refreshToken) {
return fromPromise(
getNewToken(client)
.then(({ data: { refreshToken } }) => {
console.log("Promise data: ", refreshToken);
localStorage.setItem("token", refreshToken.token);
localStorage.setItem("refreshToken", refreshToken.refreshToken);
return refreshToken.token;
})
.catch((error) => {
// Handle token refresh errors e.g clear stored tokens, redirect to login, ...
console.log("Error after setting token: ", error);
return;
})
)
.filter((value) => {
console.log("In filter: ", value);
return Boolean(value);
})
.flatMap(() => {
console.log("In flat map");
// retry the request, returning the new observable
return forward(operation);
});
}
@adrianolsk :这种方法似乎总是在令牌过期之前刷新令牌,在某些身份验证服务(例如 Auth0 的checkSession )的情况下,这将为每个 GraphQL 请求进行不必要的 Auth0 服务器往返。
我正在尝试使用
fromPromise
异步刷新令牌。
基本上遵循这篇文章的第三个框我成功地获取和存储了令牌,但是
catch
或filter
或flatMap
都没有被调用。 我不确定如何调试它,所以一些建议会有所帮助。if (token && refreshToken) { return fromPromise( getNewToken(client) .then(({ data: { refreshToken } }) => { console.log("Promise data: ", refreshToken); localStorage.setItem("token", refreshToken.token); localStorage.setItem("refreshToken", refreshToken.refreshToken); return refreshToken.token; }) .catch((error) => { // Handle token refresh errors e.g clear stored tokens, redirect to login, ... console.log("Error after setting token: ", error); return; }) ) .filter((value) => { console.log("In filter: ", value); return Boolean(value); }) .flatMap(() => { console.log("In flat map"); // retry the request, returning the new observable return forward(operation); }); }
我已经找到了错误的原因。 在上面的代码中没有看到,但我使用了map
函数来映射每个结果错误。 这导致onError
不返回任何内容,并且 observable 没有订阅令牌更新操作。
相当混乱,我花了很长时间才弄明白。 感谢博文的作者对我的帮助。
ERROR Error: Network error: Cannot read property 'length' of null
@WilsonLau0755 ,我遇到了同样的问题。 通过将所有null
标头设置为空字符串''
来解决它。
为什么 onError 不仅可用于 async await?
最有用的评论
看起来
onError
回调不接受aync
函数或Promise
返回。 见代码https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74之前曾报告过此问题:#190
我认为如果
apollo-link-error
可以处理Promise
会更好,类似于apollo-link-retry
在这里所做的:#436