Ярлыки выпуска
Вопрос
Здесь упоминается точный сценарий, который я пытаюсь реализовать:
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, любая помощь приветствуется!
Если вы больше знакомы с обещаниями, вы можете использовать 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
Дальнейшая проверка показывает, что onError
ссылки Apollo вызывается дважды при использовании приведенного выше кода. Даже ограничение однократного выполнения обещания refresh token
не устраняет ошибку.
Что происходит:
1) Начальный запрос выполняется
2) Он терпит неудачу и запускает ссылку apollo onError
3) ?? Он снова запускает ссылку Apollo onError
4) Обещание обновить токен, в 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
Мой стек ссылок apollo теперь выглядит следующим образом:
[
refreshTokenLink,
requestLink,
batchHttpLink
]
refreshTokenLink
всегда вызывается для проверки токена доступа перед выполнением любого ответа на конечную точку graphql и работает как шарм.
К сожалению, это предполагает, что вызов конечной точки graphql всегда должен быть аутентифицирован (что в моем случае так и есть).
Похоже, что onError
callback не принимает aync
function или 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 с реагирующим родным, мне нужно удалить некоторый токен из AsyncStorage onError, поэтому он должен быть асинхронной функцией
Это решение сработало для меня: https://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 спасибо за ваш пример, действительно помогает. Однако есть небольшая проблема: subscriber
является недопустимым возвращаемым значением в соответствии с типами Observable
: оно должно быть 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: Я добавил пиар по этому поводу: https://github.com/apollographql/apollo-link/pull/825
Эта проблема занимает высокое место в Google, поэтому я поделюсь своим решением здесь, чтобы помочь некоторым людям: https://gist.github.com/alfonmga/9602085094651c03cd2e270da9b2e3f7
Я пробовал ваше решение, но столкнулся с новой проблемой:
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) не обновляется, что означает, что каждый отдельный запрос к серверу будет использовать старый токен доступа (и впоследствии нужно будет обновить еще раз).
По какой - то причине , я получаю следующее сообщение об ошибке , когда я пытаюсь обновить печенье во время обновления (я создал новый вопрос здесь об этом):
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. Это асинхронная функция, возвращающая обещание):
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 для токенов обновления. Для SSR с использованием nextjs я собираю данные из всех запросов 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
Я думаю, что заголовки «Авторизация в ...» могут конфликтовать с новой авторизацией.
проблема выше находится в 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)
}
})
Привет! Я использую полный транспорт веб-сокетов, нужно запросить токен. Не знаю, как это сделать.
Я хочу сделать запрос на получение, когда сервер отвечает, что срок действия 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, мы опубликовали обходные пути выше, взгляните на наблюдаемые
Я не могу обновить токен, может ли кто-нибудь предоставить рабочий пример
@ 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, можете ли вы предоставить сервисный код, написанный
импортировать AuthService из 'services / auth-service' // это моя реализация
const {токен} = ждать AuthService.getCredentials ();
когда я пытаюсь импортировать сервис, я получаю ошибки
Это моя служба, она просто считывает AsyncStorage из response-native, поэтому после входа в систему я устанавливаю значение там, и перед каждым запросом код просто захватывает информацию и задает ее в заголовке, вы можете сделать то же самое, или используя 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's checkSession ) сделает ненужный обход сервера Auth0 для каждого запроса GraphQL.
Я пытаюсь использовать
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
ничего не вернуло, а наблюдаемый объект не был подписан на операцию обновления токена.
Довольно запутанно, и мне потребовалось так много времени, чтобы понять это. Спасибо автору сообщения в блоге за помощь.
ERROR Error: Network error: Cannot read property 'length' of null
@ WilsonLau0755 , у меня была такая же проблема. Решил это, установив все заголовки null
в пустую строку ''
.
Почему onError нельзя использовать только с async await?
Самый полезный комментарий
Похоже, что
onError
callback не принимаетaync
function или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