Emitir etiquetas
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!
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:
onError
se maneja a través de la lógica promiseToObservable
y se vuelve a intentar.onRefreshToken
, el middleware ya está en la segunda ejecución con el token anterior.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
ofilter
oflatMap
. 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?
Comentario más útil
Parece que
onError
callback no aceptaaync
función oPromise
devoluciones. Ver código https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74Este problema se informó antes: # 190
Creo que funcionaría mejor si
apollo-link-error
puede lidiar conPromise
, similar a lo queapollo-link-retry
hace aquí: # 436