<p>apollo-link-error - wie asynchrone Aktualisierung eines Tokens?</p>

Erstellt am 15. Juni 2018  ·  31Kommentare  ·  Quelle: apollographql/apollo-link

Problemetiketten

  • [ ] hat-Reproduktion
  • [ ] Merkmal
  • [x] Dokumente
  • [ ] Blockieren <----- Entschuldigung, nicht blockieren
  • [ ] gute erste Ausgabe

Frage

Das genaue Szenario, das ich erreichen möchte, wird hier erwähnt:

https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-error#retrying -failed-requests

            operation.setContext({
              headers: {
                ...oldHeaders,
                authorization: getNewToken(),
              },
            });

Wenn jedoch ein Token abgelaufen ist, sollte ein asynchrones resfreshToken aufgerufen und "wartet" werden, bevor getNewToken ein gültiges Authentifizierungstoken zurückgeben kann. Ich denke.

Meine Frage ist, wie man einen asynchronen resfreshToken Aufruf durchführt. Ich habe await refreshToken() versucht (was ein Versprechen auflöst, wenn es abgeschlossen ist), aber aus meinen protokollierten Stack-Traces scheint es, dass dies ziemlich viel mit RxJS durcheinanderbringt. Ich bin ein RxJS n00b, jede Hilfe wird sehr geschätzt!

blocking docs

Hilfreichster Kommentar

Es sieht so aus, als ob onError Callback keine aync Funktion oder Promise Rückgaben akzeptiert. Siehe Code https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74

Dieses Problem wurde bereits gemeldet: #190

Ich denke, es würde besser funktionieren, wenn apollo-link-error mit Promise umgehen kann, ähnlich wie es apollo-link-retry hier tut: #436

Alle 31 Kommentare

Wenn Sie mit Versprechen besser vertraut sind, können Sie den fromPromise Helfer verwenden

import { fromPromise } from 'apollo-link';

return fromPromise(refreshToken().then(token => {
  operation.setContext({
    headers: {
      ...oldHeaders,
      authorization: token,
    },
  });
  return forward(operation);
}))

@thymikee Habe deine Lösung

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

Eine weitere Untersuchung zeigt, dass onError des Apollo-Links zweimal aufgerufen wird, wenn der obige Code verwendet wird. Selbst die Begrenzung des refresh token Versprechens, einmal ausgeführt zu werden, behebt den Fehler nicht.

Was passiert ist:

1) Erstabfrage wird ausgeführt
2) Es schlägt fehl und führt den apollo-Link onError
3) ?? Es läuft wieder apollos Link onError
4) Versprechen, Token zu aktualisieren, in onError beendet die Ausführung und wird aufgelöst.
5) (Die anfängliche Abfrage wird kein zweites Mal ausgeführt, nachdem die Zusage erfolgreich war)
6) Die anfängliche Abfrage gibt ein Ergebnis mit data als undefiniert zurück

Wir hoffen, dass jemand eine Lösung dafür findet, sonst müssen wir wieder langlebige Zugriffstoken verwenden, anstatt sie nach Ablauf zu aktualisieren.

Wenn Ihre Token-Abruflogik korrekt ist, sollte onError nur einmal aufgerufen werden. Offenbar haben Sie Probleme mit Ihrer Token-Abfrage

@thymikee Async- Anfrage mit einem Dummy-Versprechen

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

Bearbeiten: fromPromise und es funktioniert korrekt. Irgendwie endet die Verarbeitung des Link-Stacks, bevor das Ergebnis zurückgegeben wird, sodass forward(operation) nicht ausgeführt wird.

Nach der Analyse des fromPromise Codes und des Commit #172 kann fromPromise nur in Vermutungen mit einem Apollo Link Objekt verwendet werden.

Bei der Recherche nach einer Lösung bin ich schließlich auf dieses Projekt gestoßen: apollo-link-token-refresh

Mein apollo-Link-Stack sieht jetzt wie folgt aus:

[
   refreshTokenLink,
   requestLink,
   batchHttpLink
]

refreshTokenLink wird immer aufgerufen, um das Zugriffstoken zu überprüfen, bevor eine Antwort an den graphql-Endpunkt ausgeführt wird, und funktioniert wie ein Zauber.

Dies setzt leider voraus, dass der Aufruf des graphql-Endpunkts immer authentifiziert werden muss (was in meinem Fall der Fall ist).

Es sieht so aus, als ob onError Callback keine aync Funktion oder Promise Rückgaben akzeptiert. Siehe Code https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74

Dieses Problem wurde bereits gemeldet: #190

Ich denke, es würde besser funktionieren, wenn apollo-link-error mit Promise umgehen kann, ähnlich wie es apollo-link-retry hier tut: #436

Wenn ich das gleiche Problem habe, muss ich bei Verwendung von apollo mit React native ein Token aus AsyncStorage onError entfernen, also muss es eine asynchrone Funktion sein

Diese Lösung hat bei mir funktioniert: https://stackoverflow.com/a/51321068/60223

Ich habe dies gelöst, indem ich ein Dienstprogramm 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
  });

und dann

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 danke für dein Beispiel, es hilft wirklich. Es gibt jedoch ein kleines Problem damit: subscriber ist ein ungültiger Rückgabewert gemäß der Eingabe von Observable : es sollte eher 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;
}

dh es ist sicher, stattdessen undefined zurückzugeben. Ich denke, es sollte in der README-Datei des Projekts erwähnt werden.

UPD: Ich habe dazu eine PR hinzugefügt: https://github.com/apollographql/apollo-link/pull/825

Dieses Problem hat bei Google einen hohen Stellenwert, daher teile ich meine Lösung hier, um einigen Leuten zu helfen: https://gist.github.com/alfonmga/9602085094651c03cd2e270da9b2e3f7

Ich habe deine Lösung ausprobiert, stehe aber vor einem neuen Problem:

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'

Wie speichern Sie das neue Authentifizierungstoken, wenn es aktualisiert wurde?

Sicher, ich kann neue Header in der Wiederholungsanfrage setzen, aber das ursprüngliche Zugriffstoken (das ich in Cookies speichere) wird nicht aktualisiert, was bedeutet, dass jede einzelne Anfrage an den Server das alte Zugriffstoken verwendet (und anschließend .) muss noch einmal aufgefrischt werden).

Aus irgendeinem Grund , warum ich die folgende Fehlermeldung erhalten , wenn ich versuche die Cookies während des Refresh zu aktualisieren (habe ich ein neues Problem hier darüber):

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 vielleicht würde dir das helfen https://stackoverflow.com/questions/55356736/change-apollo-client-options-for-jwt-token Ich habe ein ähnliches Problem beim Aktualisieren des Tokens

Hallo, danke @crazy4groovy. Ich habe Ihre Lösung ausprobiert, aber ich habe immer noch das Problem, dass die Middleware, bei der ich das Token an die graphql-Anfrage anhänge, aufgerufen wird, bevor das neue Token auf die Anfrage gesetzt wird. Daher enthält der Header immer noch das ungültige Token.

Ein paar Hintergrundinfos: Wir erhalten einen Netzwerkfehler, wenn das Token ungültig ist, und über ein Refresh-Token können wir ein neues erhalten und es erneut versuchen. Da die Middleware jedoch aufgerufen wird, bevor das Aktualisierungstoken gesammelt und auf lokalen Speicher gesetzt wird, enthält sie immer noch das ungültige Token. Die Token-Aktualisierungslogik funktioniert einwandfrei, da wir dann am Ende den neuen Token-Satz erhalten. Ich habe das Problem ein wenig debuggt und das Timing ist wie folgt:

  • Middleware: Ungültiges Token angehängt
  • Anfrage für eine Anfrage wird gestellt
  • Netzwerkfehler: 401, weil Token ungültig, wird abgefangen und in onError es über die promiseToObservable Logik behandelt und erneut versucht.
  • Bis wir damit fertig sind, den Token im onRefreshToken Versprechen zu erhalten, befindet sich die Middleware bereits im zweiten Durchlauf mit dem alten Token.
  • Token wird im lokalen Speicher aktualisiert...

Hier ist ein Ausschnitt dieser Teile (wobei onRefreshtoken übersprungen wird. Es ist eine asynchrone Funktion, die ein Versprechen zurückgibt):

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

Könnten Sie mir bitte sagen, was ich hier vermisse? Vielen Dank im Voraus...

Okay, sorry für die Verwirrung, falls vorhanden...

Ich habe die Antwort gefunden und es ist eine dumme, nachdem ich stundenlang danach gesucht und - natürlich - gefunden habe, nachdem ich hier gepostet habe: während der Initialisierung des apollo-Clients habe ich Middleware und Fehlerlink getauscht. Jetzt funktioniert es. Fehlerlink sollte natürlich zuerst sein..
alt: link: from([authMiddleware, errorLink, /* others */])
neu: link: from([errorLink, authMiddleware, /* others */])

Entschuldigung nochmal..

Hallo Leute,

Ich habe das folgende Problem bei der Verwendung von onError für Aktualisierungstoken. Zum Zweck von SSR mit nextjs sammle ich Daten aus allen graphql-Abfragen, aber was passiert, wenn wir zum Beispiel 2 Abfragen haben und jede von ihnen mit einem Fehler endet, weil das jwt-Token abgelaufen ist. Dann feuert es zweimal den onError und wir fordern zweimal Aktualisierungstoken an, was teuer ist. Ich kann mir nicht vorstellen, woher das Problem kommen könnte. Hier ist der Code, den ich verwende. Können Sie bitte dabei helfen.

https://gist.github.com/shaxaaa/15817f1bcc7b479f3c541383d2e83650

Ich habe ein bisschen mit diesem Problem gerungen, aber endlich habe ich es zum Laufen gebracht. Ich warf ein Paket zusammen.

https://github.com/baleeds/apollo-link-refresh-token

Der Hauptunterschied zwischen diesem Paket und dem Paket namens apollo-link-token-refresh besteht darin, dass dieses Paket auf einen Netzwerkfehler wartet, bevor es eine Aktualisierung versucht.

Lassen Sie es mich wissen, wenn Sie Ideen für Änderungen haben.

Hier ist die grundlegende Verwendung:

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

Ich habe dies gelöst, indem ich ein Dienstprogramm 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
  });

und dann

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

Ich benutze es und habe festgestellt, dass es nach der Aktualisierungs-Token-Anforderung immer noch ein altes Token verwendet. also ich versuche es wie folgt:

return promiseToObservable(refreshToken()).flatMap((value) => {
  operation.setContext(({ headers = {} }) => ({
    headers: {
      // re-add old headers
      // ...headers,
      Authorization: `JWT ${value.token}`
    }
  }));
  return forward(operation)
});

und es funktioniert.
Es gibt jedoch immer noch ein Problem, dass, wenn ich ...headers hinzufüge (bedeutet, alte Header neu hinzuzufügen), etwas nicht stimmt, bevor die Weiterleitungsanfrage gesendet wird:
ERROR Error: Network error: Cannot read property 'length' of null
Ich denke, die Autorisierung in ...Headern kann mit der neuen Autorisierung kollidieren.

das obige problem ist in apollo-angular "apollo-angular-link-http": "^1.6.0", und nicht in apollo-client "apollo-link-http": "^1.5.16", während der link-error der gleiche ist "apollo-link-error": "^1.1.12",

eine andere Syntax :augen:

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

Hallo! Ich verwende den vollständigen Websocket-Transport und muss eine Tokenabfrage anfordern. Keine Ahnung wie das geht.
Ich möchte eine Empfangsanforderung ausführen, wenn der Server antwortet, dass accessToken abgelaufen ist.

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 wir haben oben Workarounds gepostet, werfen Sie einen Blick auf Observables

Ich kann das Token nicht aktualisieren, kann jemand das Arbeitsbeispiel zur Verfügung stellen?

@ Ramyapriya24 hier ist der Code, den ich verwende.

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

@adrianosk können Sie den geschriebenen Servicecode bereitstellen

AuthService aus 'services/auth-service' importieren // das ist meine Implementierung
const { Token } = erwarten AuthService.getCredentials();

Wenn ich versuche, den Dienst zu importieren, erhalte ich Fehler

Das ist mein Service, es liest nur den AsyncStorage von Reactive-native, also setze ich nach dem Login den Wert dort und vor jeder Anfrage den Code einfach die Info abrufen und in den Header setzen, Sie können dasselbe tun oder localStorage verwenden, wenn Sie sind im Netz.

Wo speichern Sie die Informationen, die Sie verwenden möchten?

du kannst das einfach benutzen

//save the token after login or when it refreshes
localStorage.setItem('token', yourToken);

und benutze es

const asyncAuthLink = setContext(() => {
    // grab token from localStorage
    const token = localStorage.getItem('token');
    return {
      headers: {
        authorization: token
      },
    };
  },
);

@adrianolsk danke für die Erklärung, aber ich verwende Winkel. Ich kann den Dienst nicht in die Datei grapqh.module.ts importieren. Ich erhalte Fehler, wenn ich den Dienst verwende

Kann jemand wissen, wie man den Dienst in der Datei module.ts verwendet, ohne Klasse und Konstruktor zu verwenden?

Danke

Ich versuche, fromPromise für die asynchrone Aktualisierung des Tokens zu verwenden.
Im Grunde nach der dritten Box aus diesem Beitrag

Ich erhalte und speichere die Token erfolgreich, aber weder catch noch filter oder flatMap wird aufgerufen. Ich bin mir nicht sicher, wie ich das debuggen soll, daher sind einige Vorschläge hilfreich.

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 : Dieser Ansatz scheint den Token immer zu aktualisieren , sogar bevor er abgelaufen ist, was im Fall einiger Authentifizierungsdienste (z. B. checkSession von Auth0 ) einen unnötigen Auth0-Server-Roundtrip für jede GraphQL-Anfrage macht.

Ich versuche, fromPromise für die asynchrone Aktualisierung des Tokens zu verwenden.
Im Grunde nach der dritten Box aus diesem Beitrag

Ich erhalte und speichere die Token erfolgreich, aber weder catch noch filter oder flatMap wird aufgerufen. Ich bin mir nicht sicher, wie ich das debuggen soll, daher sind einige Vorschläge hilfreich.

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

Ich habe die Fehlerursache gefunden. Im obigen Code nicht zu sehen, aber ich habe eine map Funktion verwendet, um jeden der resultierenden Fehler zuzuordnen. Dies führte dazu, dass onError nichts zurückgab und die Observable wurde nicht für den Vorgang zur Tokenerneuerung abonniert.

Ziemlich verwirrend und ich habe so lange gebraucht, um es herauszufinden. Danke an den Autor des Blogbeitrags für die Hilfe.

ERROR Error: Network error: Cannot read property 'length' of null

@ WilsonLau0755 , ich hatte das gleiche Problem. Gelöst, indem alle null Header auf eine leere Zeichenfolge '' .

Warum ist onError nicht nur für die Verwendung mit Async-Await verfügbar?

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen