Étiquettes de problème
Question
Le scénario précis que j'essaie d'accomplir est mentionné ici :
https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-error#retrying -failed-requests
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
Cependant, si un jeton a expiré, un resfreshToken
asynchrone doit être appelé et "attendu", avant que getNewToken
puisse renvoyer un jeton d'authentification valide. Je pense.
Ma question est, comment faire un appel async resfreshToken
. J'ai essayé await refreshToken()
(qui résout une promesse lorsqu'elle est terminée), mais d'après mes traces de pile enregistrées, il semble que cela perturbe beaucoup RxJS. Je suis un RxJS n00b, toute aide est grandement appréciée !
Si vous êtes plus familier avec les promesses, vous pouvez utiliser fromPromise
helper
import { fromPromise } from 'apollo-link';
return fromPromise(refreshToken().then(token => {
operation.setContext({
headers: {
...oldHeaders,
authorization: token,
},
});
return forward(operation);
}))
@thymikee J'ai essayé votre solution et elle échoue avec le message suivant :
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
Une inspection plus poussée montre que le onError
du lien Apollo est appelé deux fois lors de l'utilisation du code ci-dessus. Même limiter la promesse refresh token
d'être exécuté une fois, ne résout pas l'erreur.
Ce qui se produit est:
1) La requête initiale est exécutée
2) Il échoue et exécute le lien d'Apollo onError
3) ?? Il lance à nouveau le lien d'Apollo onError
4) Promesse de rafraîchir le jeton, dans onError
termine l'exécution et est résolu.
5) (La requête initiale n'est pas exécutée une deuxième fois après la réussite de la promesse)
6) La requête initiale renvoie un résultat contenant data
comme non défini
En espérant que quelqu'un trouve une solution à cela, sinon nous devrons revenir à l'utilisation de jetons d'accès de longue durée plutôt que de les actualiser à l'expiration.
Si votre logique de récupération de jetons est correcte, onError
ne doit être appelé qu'une seule fois. Il semble que vous ayez des problèmes avec votre requête de jeton
@thymikee Changement de requête asynchrone avec une promesse factice. Échoue toujours avec le message ci-dessus et la requête initiale n'est pas exécutée deux fois. Tous les jetons sont valides au moment du test.
Code:
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));
})
)
Edit : Suppression du fromPromise
et cela fonctionne correctement. D'une manière ou d'une autre, le traitement de la pile de liens se termine avant de renvoyer le résultat, donc forward(operation)
n'est pas exécuté.
Après analyse du code fromPromise
et du commit #172 , le fromPromise
ne peut être utilisé qu'en conjecture avec un objet Apollo Link.
Après avoir recherché une solution, je suis finalement tombé sur ce projet : apollo-link-token-refresh
Ma pile de liens apollo est maintenant la suivante :
[
refreshTokenLink,
requestLink,
batchHttpLink
]
refreshTokenLink
est toujours appelé pour vérifier le jeton d'accès avant d'exécuter une réponse au point de terminaison graphql et fonctionne comme un charme.
Malheureusement, cela suppose que l'appel au point de terminaison graphql doit toujours être authentifié (ce qui est le cas, dans mon cas).
Il semble que le rappel onError
n'accepte pas la fonction aync
ou les retours Promise
. Voir le code https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74
Ce problème a déjà été signalé : #190
Je pense que cela fonctionnerait mieux si apollo-link-error
pouvait gérer Promise
, similaire à ce que apollo-link-retry
fait ici : #436
ayant le même problème, en utilisant apollo avec react native, je dois supprimer un jeton d'AsyncStorage onError, il doit donc s'agir d'une fonction asynchrone
Cette solution a fonctionné pour moi : https://stackoverflow.com/a/51321068/60223
J'ai résolu ce problème en créant un utilitaire 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
});
puis
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 merci pour votre exemple, ça aide vraiment. Cependant, il y a un petit problème avec cela : subscriber
est une valeur de retour invalide selon les frappes Observable
: elle devrait plutôt être 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;
}
c'est-à-dire qu'il est sûr de retourner undefined à la place. Je suppose que cela devrait être mentionné dans le fichier README du projet.
UPD : j'ai ajouté un PR à ce sujet : https://github.com/apollographql/apollo-link/pull/825
Ce problème occupe une place de choix sur Google, je partage donc ma solution ici pour aider certaines personnes : https://gist.github.com/alfonmga/9602085094651c03cd2e270da9b2e3f7
J'ai essayé votre solution mais je suis confronté à un nouveau problème :
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'
Comment stockez-vous le nouveau jeton d'authentification une fois qu'il est actualisé ?
Bien sûr, je peux définir de nouveaux en-têtes dans la demande de nouvelle tentative, mais le jeton d'accès d'origine (que je stocke dans les cookies) n'est pas mis à jour, ce qui signifie que chaque demande adressée au serveur utilisera l'ancien jeton d'accès (et par la suite devra être actualisé encore une fois).
Pour une raison quelconque, je reçois le message d'erreur suivant chaque fois que j'essaie de mettre à jour les cookies pendant l'actualisation (j'ai créé un nouveau problème ici à ce sujet):
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 peut-être que cela vous aiderait https://stackoverflow.com/questions/55356736/change-apollo-client-options-for-jwt-token Je rencontre un problème similaire sur la façon de mettre à jour le jeton
Salut, merci @crazy4groovy. J'ai essayé votre solution mais j'ai toujours le problème, que le middleware où j'ajoute le jeton à la demande graphql est appelé avant que le nouveau jeton ne soit défini sur la demande. Par conséquent, l'en-tête a toujours le jeton invalide.
Un peu d'informations générales : nous obtenons une erreur de réseau, lorsque le jeton n'est pas valide, et via un jeton d'actualisation, nous pouvons en obtenir un nouveau et réessayer. Mais comme le middleware est appelé avant que le jeton d'actualisation ne soit collecté et défini sur le stockage local, il a toujours celui qui n'est pas valide. La logique d'actualisation du jeton fonctionne bien, car nous obtenons ensuite le nouveau jeton défini à la fin. J'ai un peu débogué le problème et le timing est le suivant :
onError
il est géré via la logique promiseToObservable
et réessayé.onRefreshToken
, le middleware est déjà dans la deuxième exécution avec l'ancien jeton.Voici un extrait de ces parties (en sautant onRefreshtoken. C'est une fonction asynchrone, renvoyant une promesse) :
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.
}
}
);
Pourriez-vous s'il vous plaît me dire ce que je manque ici? Merci beaucoup d'avance...
OK, désolé pour la confusion, le cas échéant...
J'ai trouvé la réponse et elle est stupide après l'avoir cherchée pendant des heures et trouvé - bien sûr - après avoir posté ici : lors de l'initialisation du client apollo, j'ai échangé middleware et lien d'erreur. Maintenant ça marche. Le lien d'erreur devrait être le premier, évidemment.
ancien : link: from([authMiddleware, errorLink, /* others */])
nouveau : link: from([errorLink, authMiddleware, /* others */])
Encore pardon..
Bonjour gars,
J'ai le problème suivant en utilisant onError pour les jetons d'actualisation. Dans le but de SSR en utilisant nextjs, je collecte des données à partir de toutes les requêtes graphql, mais que se passe-t-il lorsque nous avons 2 requêtes par exemple et que chacune d'entre elles se termine par une erreur car le jeton jwt a expiré. Ensuite, il déclenche deux fois l'onError et nous appelons deux fois pour des jetons de rafraîchissement, ce qui est coûteux. Je n'arrive pas à savoir d'où peut venir le problème. Voici le code que j'utilise. Pouvez-vous s'il vous plaît aider avec cela.
https://gist.github.com/shaxaaa/15817f1bcc7b479f3c541383d2e83650
J'ai lutté un peu avec ce problème, mais j'ai finalement réussi à le faire fonctionner. J'ai jeté un paquet ensemble.
https://github.com/baleeds/apollo-link-refresh-token
La principale différence entre ce package et celui appelé apollo-link-token-refresh est que ce package attendra une erreur réseau avant de tenter une actualisation.
Faites-moi savoir si vous avez des idées de changements.
Voici l'utilisation de base :
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;
},
});
J'ai résolu ce problème en créant un utilitaire
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 });
puis
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)); } });
Je l'utilise et j'ai trouvé qu'il utilisait toujours l'ancien jeton après la demande de jeton d'actualisation. alors, j'essaye comme suit:
return promiseToObservable(refreshToken()).flatMap((value) => {
operation.setContext(({ headers = {} }) => ({
headers: {
// re-add old headers
// ...headers,
Authorization: `JWT ${value.token}`
}
}));
return forward(operation)
});
et il fonctionne.
Cependant, il y a toujours un problème que si j'ajoute le ...headers
(cela signifie rajouter les anciens en-têtes), il y a quelque chose qui ne va pas avant que la demande de transfert ne soit envoyée :
ERROR Error: Network error: Cannot read property 'length' of null
Je pense que l'autorisation dans les en-têtes ... peut entrer en conflit avec la nouvelle autorisation.
le problème ci-dessus est dans apollo-angular "apollo-angular-link-http": "^1.6.0",
et non dans apollo-client "apollo-link-http": "^1.5.16",
alors que l'erreur de lien est la même "apollo-link-error": "^1.1.12",
une autre syntaxe :yeux:
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)
}
})
Salut! J'utilise le transport Websocket complet, je dois demander une requête de jeton. Aucune idée de comment faire ça.
Je veux faire une demande de réception lorsque le serveur répond que le accessToken
a expiré.
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, nous avons publié des solutions de contournement ci-dessus, jetez un œil aux observables
Je ne parviens pas à mettre à jour le jeton, quelqu'un peut-il fournir l'exemple de travail
@ Ramyapriya24 voici le code que j'utilise.
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 pouvez-vous fournir le code de service écrit
importer AuthService de 'services/auth-service' // ceci est mon implémentation
const { jeton } = wait AuthService.getCredentials();
lorsque j'essaie d'importer le service, j'obtiens des erreurs
C'est mon service, il vient de lire l'AsyncStorage à partir de react-native, donc après la connexion, j'y ai défini la valeur et avant chaque demande, le code récupère simplement les informations et les définit dans l'en-tête, vous pouvez faire de même, ou utiliser localStorage si vous sont sur le net.
Où stockez-vous les informations que vous souhaitez utiliser ?
vous pouvez simplement utiliser ceci
//save the token after login or when it refreshes
localStorage.setItem('token', yourToken);
et l'utiliser
const asyncAuthLink = setContext(() => {
// grab token from localStorage
const token = localStorage.getItem('token');
return {
headers: {
authorization: token
},
};
},
);
@adrianolsk merci pour l'explication mais j'utilise angulaire Je ne parviens pas à importer le service dans le fichier grapqh.module.ts Je reçois des erreurs lorsque j'utilise le service
quelqu'un peut-il savoir comment utiliser le service dans le fichier module.ts sans utiliser la classe et le constructeur
Merci
J'essaie d'utiliser fromPromise
pour actualiser asynchrone le jeton.
En gros en suivant la troisième case de ce post
J'obtiens et stocke avec succès les jetons, mais aucun des catch
ou filter
ou flatMap
n'est appelé. Je ne sais pas comment déboguer cela, donc quelques suggestions seront utiles.
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 : cette approche semble toujours actualiser le jeton, même avant son expiration, ce qui, dans le cas de certains services d'authentification (par exemple, checkSession d' Auth0 ) fera un aller-retour inutile du serveur Auth0 pour chaque requête GraphQL.
J'essaie d'utiliser
fromPromise
pour actualiser asynchrone le jeton.
En gros en suivant la troisième case de ce postJ'obtiens et stocke avec succès les jetons, mais aucun des
catch
oufilter
ouflatMap
n'est appelé. Je ne sais pas comment déboguer cela, donc quelques suggestions seront utiles.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); }); }
J'ai trouvé la cause de l'erreur. Pas vu dans le code ci-dessus, mais j'ai utilisé une fonction map
pour mapper chacune des erreurs résultantes. Cela a causé le retour de onError
et l'observable n'a pas été abonné à l'opération de renouvellement du jeton.
Assez déroutant et il m'a fallu tellement de temps pour le comprendre. Merci à l'auteur du blog de m'avoir aidé.
ERROR Error: Network error: Cannot read property 'length' of null
@ WilsonLau0755 , j'ai eu le même problème. Résolu en définissant tous les en-têtes null
sur une chaîne vide ''
.
Pourquoi onError n'est-il pas uniquement disponible pour être utilisé avec async wait ?
Commentaire le plus utile
Il semble que le rappel
onError
n'accepte pas la fonctionaync
ou les retoursPromise
. Voir le code https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74Ce problème a déjà été signalé : #190
Je pense que cela fonctionnerait mieux si
apollo-link-error
pouvait gérerPromise
, similaire à ce queapollo-link-retry
fait ici : #436