<p>apollo-link-error: ¿cómo actualizar asíncronamente un token?</p>

Creado en 15 jun. 2018  ·  31Comentarios  ·  Fuente: apollographql/apollo-link

Emitir etiquetas

  • [] tiene-reproducción
  • [ ] característica
  • [x] documentos
  • [] bloqueando <----- lo siento, no bloqueando
  • [] buen primer número

Pregunta

El escenario preciso que estoy tratando de lograr se menciona aquí:

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

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

Sin embargo, si un token está vencido, se debe llamar a un resfreshToken asíncrono y "esperar", antes de que getNewToken pueda devolver un token de autenticación válido. Creo.

Mi pregunta es cómo hacer una llamada asíncrona resfreshToken . Probé await refreshToken() (que resuelve una promesa cuando está completa), pero de mis seguimientos de pila registrados parece que esto se mete bastante con RxJS. Soy un RxJS n00b, ¡cualquier ayuda es muy apreciada!

blocking docs

Comentario más útil

Parece que onError callback no acepta aync función o Promise devoluciones. Ver código https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74

Este problema se informó antes: # 190

Creo que funcionaría mejor si apollo-link-error puede lidiar con Promise , similar a lo que apollo-link-retry hace aquí: # 436

Todos 31 comentarios

Si está más familiarizado con las promesas, puede usar fromPromise helper

import { fromPromise } from 'apollo-link';

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

@thymikee Probó su solución y falla con el siguiente mensaje:

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

Una inspección más detallada muestra que onError del enlace Apollo se llama dos veces cuando se usa el código anterior. Incluso limitar la promesa refresh token de ejecutarse una vez, no soluciona el error.

Lo que sucede es:

1) Se ejecuta la consulta inicial
2) Falla y ejecuta el enlace de apollo onError
3) ?? Ejecuta el enlace de apolo onError nuevamente
4) Promesa de actualizar el token, en onError termina de ejecutarse y se resuelve.
5) (La consulta inicial no se ejecuta una segunda vez después de que la promesa sea exitosa)
6) La consulta inicial devuelve un resultado que contiene data como indefinido

Esperamos que alguien encuentre una solución a esto; de lo contrario, tendremos que volver a usar tokens de acceso de larga duración en lugar de actualizarlos al vencimiento.

Si su lógica de recuperación de tokens es correcta, onError solo se debe llamar una vez. Parece que tienes problemas con tu consulta de token.

@thymikee Cambió la solicitud asíncrona con una promesa ficticia. Aún falla con el mensaje anterior y la consulta inicial no se ejecuta dos veces. Todos los tokens son válidos en el momento de la prueba.

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: se eliminó fromPromise y funciona correctamente. De alguna manera, el procesamiento de la pila de enlaces finaliza antes de devolver el resultado, por lo que forward(operation) no se ejecuta.

Después de analizar el código fromPromise y la confirmación # 172, fromPromise solo se puede usar en conjeturas con un objeto Apollo Link.

Al investigar una solución, finalmente me topé con este proyecto: apollo-link-token-refresh

Mi pila de enlaces de Apollo ahora es la siguiente:

[
   refreshTokenLink,
   requestLink,
   batchHttpLink
]

refreshTokenLink siempre se llama para verificar el token de acceso antes de ejecutar cualquier respuesta al punto final de graphql y funciona a la perfección.

Desafortunadamente, esto supone que la llamada al punto final de graphql siempre debe estar autenticada (lo que es, en mi caso).

Parece que onError callback no acepta aync función o Promise devoluciones. Ver código https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74

Este problema se informó antes: # 190

Creo que funcionaría mejor si apollo-link-error puede lidiar con Promise , similar a lo que apollo-link-retry hace aquí: # 436

teniendo el mismo problema, usando apollo con react native, necesito eliminar algún token de AsyncStorage onError, por lo que debe ser una función asincrónica

Esta solución funcionó para mí: https://stackoverflow.com/a/51321068/60223

Resolví esto creando una utilidad 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
  });

y luego

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 gracias por tu ejemplo, realmente ayuda. Sin embargo, hay un pequeño problema: subscriber no es un valor de retorno no válido de acuerdo con Observable tipificaciones: debería 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;
}

es decir, es seguro devolver undefined en su lugar. Supongo que debería mencionarse en el archivo README del proyecto.

UPD: agregué un PR sobre esto: https://github.com/apollographql/apollo-link/pull/825

Este problema ocupa un lugar destacado en Google, por lo que estoy compartiendo mi solución aquí para ayudar a algunas personas: https://gist.github.com/alfonmga/9602085094651c03cd2e270da9b2e3f7

Probé su solución pero me enfrento a un nuevo 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'

¿Cómo están almacenando el nuevo token de autenticación una vez que se actualiza?

Claro, puedo establecer nuevos encabezados en la solicitud de reintento, sin embargo, el token de acceso original (que estoy almacenando en las cookies) no se actualiza, lo que significa que cada solicitud al servidor usará el token de acceso anterior (y posteriormente deberá actualizarse una vez más).

Por alguna razón, recibo el siguiente mensaje de error cada vez que intento actualizar las cookies durante la actualización (creé un nuevo problema aquí al respecto):

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 tal vez esto le ayude https://stackoverflow.com/questions/55356736/change-apollo-client-options-for-jwt-token Me encuentro con un problema similar sobre cómo actualizar el token

Hola, gracias @ crazy4groovy. Probé su solución, pero todavía tengo el problema, que el middleware donde agrego el token a la solicitud graphql se llama antes de que el nuevo token se establezca en la solicitud. Por lo tanto, el encabezado todavía tiene el token no válido.

Un poco de información de fondo: obtenemos un error de red, cuando el token no es válido y, a través de un token de actualización, podemos obtener uno nuevo y volver a intentarlo. Pero dado que se llama al middleware antes de que se recopile el token de actualización y se establezca en el almacenamiento local, todavía tiene el no válido. Actualizar la lógica del token funciona bien, ya que al final obtenemos el nuevo conjunto de tokens. He depurado un poco el problema y el tiempo es el siguiente:

  • Middleware: token no válido adjunto
  • Se realiza una solicitud de consulta
  • Error de red: 401, porque el token no es válido, se detecta y en onError se maneja a través de la lógica promiseToObservable y se vuelve a intentar.
  • Hasta que terminemos de obtener el token en la promesa onRefreshToken , el middleware ya está en la segunda ejecución con el token anterior.
  • El token se actualiza en el almacenamiento local ...

Aquí hay un fragmento de estas partes (omitiendo el token de actualización. Es una función asíncrona, devolviendo una promesa):

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

¿Podría decirme qué me falta aquí? Muchas gracias por adelantado...

OK, perdón por la confusión, si la hay ...

Encontré la respuesta y es estúpida después de buscarla durante horas y encontrar, por supuesto, después de publicar aquí: durante la inicialización del cliente apollo, cambié el middleware y el enlace de error. Ahora funciona. El enlace de error debe ser el primero, obviamente.
antiguo: link: from([authMiddleware, errorLink, /* others */])
nuevo: link: from([errorLink, authMiddleware, /* others */])

Lo siento de nuevo..

Hola chicos,

Tengo el siguiente problema al usar onError para actualizar tokens. Para el propósito de SSR usando nextjs, estoy recopilando datos de todas las consultas graphql, pero qué sucede cuando tenemos 2 consultas, por ejemplo, y cada una de ellas termina con un error porque el token jwt ha caducado. Luego dispara el doble de onError y estamos pidiendo dos veces para obtener tokens de actualización, lo cual es costoso. No puedo entender de dónde puede venir el problema. Aquí está el código que estoy usando. ¿Podrías ayudarme con esto?

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

Luché con este problema por un tiempo, pero finalmente lo hice funcionar. Tiré un paquete juntos.

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

La principal diferencia entre este paquete y el llamado apollo-link-token-refresh es que este paquete esperará un error de red antes de intentar una actualización.

Háganme saber si tienen ideas de cambios.

Aquí está el 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;
  },
});

Resolví esto creando una utilidad 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
  });

y luego

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

Lo uso y descubrí que todavía usa token antiguo después de la solicitud de token de actualización. entonces, intento lo siguiente:

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

y funciona.
Sin embargo, todavía tiene el problema de que si agrego ...headers (significa volver a agregar encabezados antiguos), hay algo mal antes de que se envíe la solicitud de reenvío:
ERROR Error: Network error: Cannot read property 'length' of null
Creo que la autorización en los encabezados ... puede entrar en conflicto con la nueva autorización.

el problema anterior está en apollo-angular "apollo-angular-link-http": "^1.6.0", y no en apollo-client "apollo-link-http": "^1.5.16", mientras que el error de enlace es el mismo "apollo-link-error": "^1.1.12",

otra sintaxis: ojos:

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

¡Hola! Estoy usando el transporte websocket completo, necesito solicitar una consulta de token. No tengo idea de cómo hacer eso.
Quiero hacer una solicitud de recepción cuando el servidor responde que el accessToken ha expirado.

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 publicamos soluciones arriba, eche un vistazo a los observables

No puedo actualizar el token, ¿alguien proporcionó el ejemplo de trabajo?

@ Ramyapriya24 aquí está el código que estoy 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 ¿ puede proporcionar el código de servicio que escribió?

importar AuthService desde 'services / auth-service' // esta es mi implementación
const {token} = aguardar AuthService.getCredentials ();

cuando intento importar el servicio, recibo errores

Ese es mi servicio, simplemente leyó el AsyncStorage de react-native, así que después de iniciar sesión configuré el valor allí y antes de cada solicitud, el código solo toma la información y la configura en el encabezado, puede hacer lo mismo o usar localStorage si están en la web.

¿Dónde almacena la información que desea utilizar?

puedes usar esto

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

y úsalo

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

@adrianolsk gracias por la explicación pero estoy usando angular No puedo importar el servicio en el archivo grapqh.module.ts Recibo errores cuando estoy usando el servicio

¿Alguien puede saber cómo usar el servicio en el archivo module.ts sin usar la clase y el constructor?

Gracias

Estoy tratando de usar fromPromise para la actualización asíncrona del token.
Básicamente siguiendo el tercer cuadro de esta publicación.

Estoy obteniendo y almacenando los tokens con éxito, pero no se llama a catch o filter o flatMap . No estoy seguro de cómo depurar esto, por lo que algunas sugerencias serán útiles.

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 : ese enfoque parece actualizar siempre el token, incluso antes de que expire, lo que en el caso de algunos servicios de autenticación (por ejemplo, checkSession de Auth0 ) hará un viaje de ida y vuelta innecesario al servidor Auth0 para cada solicitud de GraphQL.

Estoy tratando de usar fromPromise para la actualización asíncrona del token.
Básicamente siguiendo el tercer cuadro de esta publicación.

Estoy obteniendo y almacenando los tokens con éxito, pero no se llama a catch o filter o flatMap . No estoy seguro de cómo depurar esto, por lo que algunas sugerencias serán útiles.

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

Encontré cuál fue la causa del error. No se ve en el código anterior, pero usé una función map para mapear cada uno de los errores resultantes. Esto hizo que onError no devolviera nada y el observable no se suscribió a la operación de renovación del token.

Bastante confuso y me tomó tanto tiempo darme cuenta. Gracias al autor de la publicación del blog por ayudarme.

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

@ WilsonLau0755 , tuve el mismo problema. Lo resolvió configurando todos los encabezados null en una cadena vacía '' .

¿Por qué onError no solo está disponible para usar con async await?

¿Fue útil esta página
0 / 5 - 0 calificaciones