<p>apollo-link-error - comment actualiser asynchrone un jeton ?</p>

Créé le 15 juin 2018  ·  31Commentaires  ·  Source: apollographql/apollo-link

Étiquettes de problème

  • [ ] a-reproduction
  • [ ] caractéristique
  • [x] documents
  • [ ] blocage <----- désolé, je ne bloque pas
  • [ ] bon premier numéro

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 !

blocking docs

Commentaire le plus utile

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

Tous les 31 commentaires

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 :

  • Middleware : jeton non valide attaché
  • La demande d'interrogation est faite
  • Erreur réseau : 401, car le jeton n'est pas valide, est détecté et dans onError il est géré via la logique promiseToObservable et réessayé.
  • Jusqu'à ce que nous ayons fini d'obtenir le jeton dans la promesse onRefreshToken , le middleware est déjà dans la deuxième exécution avec l'ancien jeton.
  • Le jeton est mis à jour dans le stockage local...

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 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);
    });
}

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 ?

Cette page vous a été utile?
0 / 5 - 0 notes