Etiquetas de problemas
Pergunta
O cenário preciso que estou tentando realizar é mencionado aqui:
https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-error#retrying -failed-requests
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
No entanto, se um token expirou, um assíncrono resfreshToken
deve ser chamado e "aguardado", antes que getNewToken
possa retornar um token de autenticação válido. Eu acho que.
Minha pergunta é: como fazer uma chamada assíncrona resfreshToken
. Eu tentei await refreshToken()
(que resolve uma promessa quando ela é concluída), mas pelos meus rastreamentos de pilha registrados, parece que isso bagunça bastante o RxJS. Sou um RxJS n00b, qualquer ajuda será muito apreciada!
Se você está mais familiarizado com as promessas, pode usar o fromPromise
helper
import { fromPromise } from 'apollo-link';
return fromPromise(refreshToken().then(token => {
operation.setContext({
headers: {
...oldHeaders,
authorization: token,
},
});
return forward(operation);
}))
@thymikee tentou sua solução e ela falhou com a seguinte mensagem:
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
Uma inspeção mais detalhada mostra que onError
do link da Apollo é chamado duas vezes ao usar o código acima. Mesmo limitar a promessa de refresh token
de ser executado uma vez, não corrige o erro.
O que acontece é:
1) A consulta inicial é executada
2) Ele falha e executa o link de Apolo onError
3) ?? Ele executa o link de Apolo onError
novamente
4) Promessa de atualizar o token, em onError
termina a execução e é resolvido.
5) (A consulta inicial não é executada uma segunda vez após a promessa ser bem-sucedida)
6) A consulta inicial retorna um resultado contendo data
como indefinido
Esperamos que alguém encontre uma solução para isso; do contrário, teremos que voltar a usar tokens de acesso de longa duração em vez de atualizá-los após a expiração.
Se sua lógica de recuperação de token estiver correta, onError
deve ser chamado apenas uma vez. Parece que você tem problemas com sua consulta de token
@thymikee Solicitação assíncrona
Código:
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));
})
)
Editar: Removido o fromPromise
e funciona corretamente. De alguma forma, o processamento da pilha de links termina antes de retornar o resultado, portanto forward(operation)
não é executado.
Depois de analisar o código fromPromise
e o commit # 172, o fromPromise
só pode ser usado em conjecturas com um objeto Apollo Link.
Ao pesquisar uma solução, finalmente me deparei com este projeto: apollo-link-token-refresh
Minha pilha de links Apollo agora é a seguinte:
[
refreshTokenLink,
requestLink,
batchHttpLink
]
refreshTokenLink
é sempre chamado para verificar o token de acesso antes de executar qualquer resposta ao endpoint graphql e funciona perfeitamente.
Infelizmente, isso pressupõe que a chamada para o endpoint graphql deve sempre ser autenticada (o que é, no meu caso).
Parece que onError
callback não aceita aync
function ou Promise
return. Veja o código https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74
Este problema foi relatado antes: # 190
Acho que funcionaria melhor se apollo-link-error
pudesse lidar com Promise
, semelhante ao que apollo-link-retry
faz aqui: # 436
tendo o mesmo problema, usando Apollo com react nativo, preciso remover algum token de AsyncStorage onError, então ele precisa ser uma função assíncrona
Esta solução funcionou para mim: https://stackoverflow.com/a/51321068/60223
Resolvi isso criando um utilitário 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
});
e então
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 obrigado pelo seu exemplo, realmente ajuda. No entanto, há um pequeno problema com isso: subscriber
é um valor de retorno inválido de acordo com Observable
tipificações: deveria ser 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;
}
ou seja, é seguro retornar indefinido. Acho que deve ser mencionado no arquivo README do projeto.
UPD: adicionei um PR sobre isso: https://github.com/apollographql/apollo-link/pull/825
Esse problema tem uma alta classificação no Google, então estou compartilhando minha solução aqui para ajudar algumas pessoas: https://gist.github.com/alfonmga/9602085094651c03cd2e270da9b2e3f7
Tentei sua solução, mas estou enfrentando um novo problema:
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'
Como vocês estão armazenando o novo token de autenticação depois de atualizado?
Claro, posso definir novos cabeçalhos na solicitação de nova tentativa, no entanto, o token de acesso original (que estou armazenando em cookies) não é atualizado, o que significa que cada solicitação ao servidor usará o token de acesso antigo (e posteriormente precisará ser atualizado novamente).
Por algum motivo, estou recebendo a seguinte mensagem de erro sempre que tento atualizar os cookies durante a atualização (criei um novo problema aqui sobre isso):
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 talvez isso ajude você https://stackoverflow.com/questions/55356736/change-apollo-client-options-for-jwt-token Encontro um problema semelhante sobre como atualizar o token
Olá, obrigado @ crazy4groovy. Tentei sua solução, mas ainda estou tendo o problema, que o middleware onde anexei o token à solicitação graphql é chamado antes que o novo token seja definido para a solicitação. Portanto, o cabeçalho ainda tem o token inválido.
Algumas informações básicas: obtemos um erro de rede, quando o token é inválido e, por meio de um token de atualização, podemos obter um novo e tentar novamente. Mas, como o middleware é chamado antes que o token de atualização seja coletado e configurado para armazenamento local, ele ainda possui o inválido. A lógica de atualização do token funciona bem, já que obtemos o novo conjunto de tokens no final. Eu depurei o problema um pouco e o tempo é o seguinte:
onError
ele foi tratado por meio da lógica promiseToObservable
e tentado novamente.onRefreshToken
, o middleware já está na segunda execução com o token antigo.Aqui está um snippet dessas partes (pulando onRefreshtoken. É uma função assíncrona, retornando uma promessa):
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.
}
}
);
Você poderia me dizer o que estou perdendo aqui? Muito obrigado antecipadamente ...
OK, desculpe a confusão, se houver ...
Eu encontrei a resposta, e é estúpida depois de procurar por ela por horas e descobrir - é claro - depois de postar aqui: durante a inicialização do cliente apollo, troquei o middleware e o link de erro. Agora funciona. O link de erro deve ser o primeiro, obviamente ..
antigo: link: from([authMiddleware, errorLink, /* others */])
novo: link: from([errorLink, authMiddleware, /* others */])
Desculpe de novo..
Ola pessoal,
Tenho o seguinte problema ao usar onError para tokens de atualização. Para o propósito de SSR usando nextjs, estou coletando dados de todas as consultas do Graphql, mas o que acontece quando temos 2 consultas, por exemplo, e cada uma delas termina com um erro porque o token jwt expirou. Em seguida, ele dispara duas vezes o onError e estamos chamando duas vezes para tokens de atualização, o que é caro. Não consigo descobrir de onde pode vir o problema. Aqui está o código que estou usando. Você pode, por favor, ajudar com isso.
https://gist.github.com/shaxaaa/15817f1bcc7b479f3c541383d2e83650
Lutei um pouco com esse problema, mas finalmente consegui fazer funcionar. Eu joguei um pacote juntos.
https://github.com/baleeds/apollo-link-refresh-token
A principal diferença entre este pacote e aquele chamado apollo-link-token-refresh é que este pacote irá esperar por um erro de rede antes de tentar uma atualização.
Deixe-me saber se vocês têm ideias para mudanças.
Aqui está o uso básico:
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;
},
});
Resolvi isso criando um utilitário
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 });
e então
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)); } });
Eu o uso e descobri que ele ainda usa o token antigo após a solicitação do token de atualização. então, eu tento o seguinte:
return promiseToObservable(refreshToken()).flatMap((value) => {
operation.setContext(({ headers = {} }) => ({
headers: {
// re-add old headers
// ...headers,
Authorization: `JWT ${value.token}`
}
}));
return forward(operation)
});
e funciona.
No entanto, ainda existe o problema de que se eu adicionar ...headers
(significa adicionar novamente os cabeçalhos antigos), há algo errado antes do envio da solicitação de encaminhamento:
ERROR Error: Network error: Cannot read property 'length' of null
Acho que a autorização nos cabeçalhos ... pode entrar em conflito com a nova autorização.
o problema acima está no apolo-angular "apollo-angular-link-http": "^1.6.0",
e não no apolo-cliente "apollo-link-http": "^1.5.16",
enquanto link-error é o mesmo "apollo-link-error": "^1.1.12",
outra sintaxe: olhos:
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)
}
})
Oi! Estou usando o transporte completo do websocket, preciso solicitar a consulta do token. Não tenho ideia de como fazer isso.
Quero fazer uma solicitação de recebimento quando o servidor responder que accessToken
expirou.
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 , postamos soluções alternativas acima, dê uma olhada nos observáveis
Não consigo atualizar o token; alguém pode fornecer o exemplo de trabalho
@ Ramyapriya24 aqui está o código que estou usando.
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 você pode fornecer o código de serviço escrito
importar AuthService de 'services / auth-service' // esta é minha implementação
const {token} = espera AuthService.getCredentials ();
quando tento importar o serviço, recebo erros
Esse é o meu serviço, ele apenas lê o AsyncStorage do react-native, então após o login eu defino o valor lá e antes de cada requisição o código apenas pega a informação e define no cabeçalho, você poderia fazer o mesmo, ou usando localStorage se você estão na web.
Onde você está armazenando as informações que deseja usar?
você pode apenas usar isso
//save the token after login or when it refreshes
localStorage.setItem('token', yourToken);
e usá-lo
const asyncAuthLink = setContext(() => {
// grab token from localStorage
const token = localStorage.getItem('token');
return {
headers: {
authorization: token
},
};
},
);
@adrianolsk obrigado pela explicação, mas estou usando angular não consigo importar o serviço no arquivo grapqh.module.ts Estou recebendo erros quando estou usando o serviço
Alguém pode saber como usar o serviço no arquivo module.ts sem usar a classe e o construtor
Obrigado
Estou tentando usar fromPromise
para atualização assíncrona do token.
Basicamente seguindo a terceira caixa desta postagem
Estou obtendo e armazenando os tokens com sucesso, mas nenhum dos catch
ou filter
ou flatMap
é chamado. Não tenho certeza de como depurar isso, então algumas sugestões serão úteis.
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 : essa abordagem parece sempre atualizar o token, mesmo antes de expirar, o que no caso de alguns serviços de autenticação (por exemplo, checkSession do Auth0 ) fará uma viagem de ida e volta desnecessária ao servidor Auth0 para cada solicitação GraphQL.
Estou tentando usar
fromPromise
para atualização assíncrona do token.
Basicamente seguindo a terceira caixa desta postagemEstou obtendo e armazenando os tokens com sucesso, mas nenhum dos
catch
oufilter
ouflatMap
é chamado. Não tenho certeza de como depurar isso, então algumas sugestões serão úteis.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); }); }
Eu descobri qual foi a causa do erro. Não visto no código acima, mas usei uma função map
para mapear cada um dos erros resultantes. Isso fez com que onError
não retornasse nada e o observável não fosse inscrito na operação de renovação de token.
Muito confuso e demorei muito para descobrir. Obrigado ao autor da postagem do blog por me ajudar.
ERROR Error: Network error: Cannot read property 'length' of null
@ WilsonLau0755 , tive o mesmo problema. Resolvido definindo todos os cabeçalhos null
para uma string vazia ''
.
Por que onError não está disponível apenas para uso com async await?
Comentários muito úteis
Parece que
onError
callback não aceitaaync
function ouPromise
return. Veja o código https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74Este problema foi relatado antes: # 190
Acho que funcionaria melhor se
apollo-link-error
pudesse lidar comPromise
, semelhante ao queapollo-link-retry
faz aqui: # 436