<p>apollo-link-error - como assíncrono atualizar um token?</p>

Criado em 15 jun. 2018  ·  31Comentários  ·  Fonte: apollographql/apollo-link

Etiquetas de problemas

  • [] tem-reprodução
  • [ ] característica
  • [x] docs
  • [] bloqueando <----- desculpe, não estou bloqueando
  • [] boa primeira edição

Pergunta

O cenário preciso que estou tentando realizar é mencionado aqui:

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

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

No entanto, se um token expirou, um assíncrono resfreshToken deve ser chamado e "aguardado", antes que getNewToken possa retornar um token de autenticação válido. Eu acho que.

Minha pergunta é: como fazer uma chamada assíncrona resfreshToken . Eu tentei await refreshToken() (que resolve uma promessa quando ela é concluída), mas pelos meus rastreamentos de pilha registrados, parece que isso bagunça bastante o RxJS. Sou um RxJS n00b, qualquer ajuda será muito apreciada!

blocking docs

Comentários muito úteis

Parece que onError callback não aceita aync function ou Promise return. Veja o código https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74

Este problema foi relatado antes: # 190

Acho que funcionaria melhor se apollo-link-error pudesse lidar com Promise , semelhante ao que apollo-link-retry faz aqui: # 436

Todos 31 comentários

Se você está mais familiarizado com as promessas, pode usar o fromPromise helper

import { fromPromise } from 'apollo-link';

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

@thymikee tentou sua solução e ela falhou com a seguinte mensagem:

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

Uma inspeção mais detalhada mostra que onError do link da Apollo é chamado duas vezes ao usar o código acima. Mesmo limitar a promessa de refresh token de ser executado uma vez, não corrige o erro.

O que acontece é:

1) A consulta inicial é executada
2) Ele falha e executa o link de Apolo onError
3) ?? Ele executa o link de Apolo onError novamente
4) Promessa de atualizar o token, em onError termina a execução e é resolvido.
5) (A consulta inicial não é executada uma segunda vez após a promessa ser bem-sucedida)
6) A consulta inicial retorna um resultado contendo data como indefinido

Esperamos que alguém encontre uma solução para isso; do contrário, teremos que voltar a usar tokens de acesso de longa duração em vez de atualizá-los após a expiração.

Se sua lógica de recuperação de token estiver correta, onError deve ser chamado apenas uma vez. Parece que você tem problemas com sua consulta de token

@thymikee Solicitação assíncrona

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: Removido o fromPromise e funciona corretamente. De alguma forma, o processamento da pilha de links termina antes de retornar o resultado, portanto forward(operation) não é executado.

Depois de analisar o código fromPromise e o commit # 172, o fromPromise só pode ser usado em conjecturas com um objeto Apollo Link.

Ao pesquisar uma solução, finalmente me deparei com este projeto: apollo-link-token-refresh

Minha pilha de links Apollo agora é a seguinte:

[
   refreshTokenLink,
   requestLink,
   batchHttpLink
]

refreshTokenLink é sempre chamado para verificar o token de acesso antes de executar qualquer resposta ao endpoint graphql e funciona perfeitamente.

Infelizmente, isso pressupõe que a chamada para o endpoint graphql deve sempre ser autenticada (o que é, no meu caso).

Parece que onError callback não aceita aync function ou Promise return. Veja o código https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74

Este problema foi relatado antes: # 190

Acho que funcionaria melhor se apollo-link-error pudesse lidar com Promise , semelhante ao que apollo-link-retry faz aqui: # 436

tendo o mesmo problema, usando Apollo com react nativo, preciso remover algum token de AsyncStorage onError, então ele precisa ser uma função assíncrona

Esta solução funcionou para mim: https://stackoverflow.com/a/51321068/60223

Resolvi isso criando um utilitário 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
  });

e então

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 obrigado pelo seu exemplo, realmente ajuda. No entanto, há um pequeno problema com isso: subscriber é um valor de retorno inválido de acordo com Observable tipificações: deveria 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;
}

ou seja, é seguro retornar indefinido. Acho que deve ser mencionado no arquivo README do projeto.

UPD: adicionei um PR sobre isso: https://github.com/apollographql/apollo-link/pull/825

Esse problema tem uma alta classificação no Google, então estou compartilhando minha solução aqui para ajudar algumas pessoas: https://gist.github.com/alfonmga/9602085094651c03cd2e270da9b2e3f7

Tentei sua solução, mas estou enfrentando um novo 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'

Como vocês estão armazenando o novo token de autenticação depois de atualizado?

Claro, posso definir novos cabeçalhos na solicitação de nova tentativa, no entanto, o token de acesso original (que estou armazenando em cookies) não é atualizado, o que significa que cada solicitação ao servidor usará o token de acesso antigo (e posteriormente precisará ser atualizado novamente).

Por algum motivo, estou recebendo a seguinte mensagem de erro sempre que tento atualizar os cookies durante a atualização (criei um novo problema aqui sobre isso):

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 talvez isso ajude você https://stackoverflow.com/questions/55356736/change-apollo-client-options-for-jwt-token Encontro um problema semelhante sobre como atualizar o token

Olá, obrigado @ crazy4groovy. Tentei sua solução, mas ainda estou tendo o problema, que o middleware onde anexei o token à solicitação graphql é chamado antes que o novo token seja definido para a solicitação. Portanto, o cabeçalho ainda tem o token inválido.

Algumas informações básicas: obtemos um erro de rede, quando o token é inválido e, por meio de um token de atualização, podemos obter um novo e tentar novamente. Mas, como o middleware é chamado antes que o token de atualização seja coletado e configurado para armazenamento local, ele ainda possui o inválido. A lógica de atualização do token funciona bem, já que obtemos o novo conjunto de tokens no final. Eu depurei o problema um pouco e o tempo é o seguinte:

  • Middleware: token inválido anexado
  • Solicitação de consulta é feita
  • Erro de rede: 401, porque o token inválido, foi capturado e em onError ele foi tratado por meio da lógica promiseToObservable e tentado novamente.
  • Até terminarmos de obter o token na promessa onRefreshToken , o middleware já está na segunda execução com o token antigo.
  • O token é atualizado no armazenamento local ...

Aqui está um snippet dessas partes (pulando onRefreshtoken. É uma função assíncrona, retornando uma promessa):

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

Você poderia me dizer o que estou perdendo aqui? Muito obrigado antecipadamente ...

OK, desculpe a confusão, se houver ...

Eu encontrei a resposta, e é estúpida depois de procurar por ela por horas e descobrir - é claro - depois de postar aqui: durante a inicialização do cliente apollo, troquei o middleware e o link de erro. Agora funciona. O link de erro deve ser o primeiro, obviamente ..
antigo: link: from([authMiddleware, errorLink, /* others */])
novo: link: from([errorLink, authMiddleware, /* others */])

Desculpe de novo..

Ola pessoal,

Tenho o seguinte problema ao usar onError para tokens de atualização. Para o propósito de SSR usando nextjs, estou coletando dados de todas as consultas do Graphql, mas o que acontece quando temos 2 consultas, por exemplo, e cada uma delas termina com um erro porque o token jwt expirou. Em seguida, ele dispara duas vezes o onError e estamos chamando duas vezes para tokens de atualização, o que é caro. Não consigo descobrir de onde pode vir o problema. Aqui está o código que estou usando. Você pode, por favor, ajudar com isso.

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

Lutei um pouco com esse problema, mas finalmente consegui fazer funcionar. Eu joguei um pacote juntos.

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

A principal diferença entre este pacote e aquele chamado apollo-link-token-refresh é que este pacote irá esperar por um erro de rede antes de tentar uma atualização.

Deixe-me saber se vocês têm ideias para mudanças.

Aqui está o 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;
  },
});

Resolvi isso criando um utilitário 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
  });

e então

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

Eu o uso e descobri que ele ainda usa o token antigo após a solicitação do token de atualização. então, eu tento o seguinte:

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

e funciona.
No entanto, ainda existe o problema de que se eu adicionar ...headers (significa adicionar novamente os cabeçalhos antigos), há algo errado antes do envio da solicitação de encaminhamento:
ERROR Error: Network error: Cannot read property 'length' of null
Acho que a autorização nos cabeçalhos ... pode entrar em conflito com a nova autorização.

o problema acima está no apolo-angular "apollo-angular-link-http": "^1.6.0", e não no apolo-cliente "apollo-link-http": "^1.5.16", enquanto link-error é o mesmo "apollo-link-error": "^1.1.12",

outra sintaxe: olhos:

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

Oi! Estou usando o transporte completo do websocket, preciso solicitar a consulta do token. Não tenho ideia de como fazer isso.
Quero fazer uma solicitação de recebimento quando o servidor responder que accessToken expirou.

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 , postamos soluções alternativas acima, dê uma olhada nos observáveis

Não consigo atualizar o token; alguém pode fornecer o exemplo de trabalho

@ Ramyapriya24 aqui está o código que estou 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 você pode fornecer o código de serviço escrito

importar AuthService de 'services / auth-service' // esta é minha implementação
const {token} = espera AuthService.getCredentials ();

quando tento importar o serviço, recebo erros

Esse é o meu serviço, ele apenas lê o AsyncStorage do react-native, então após o login eu defino o valor lá e antes de cada requisição o código apenas pega a informação e define no cabeçalho, você poderia fazer o mesmo, ou usando localStorage se você estão na web.

Onde você está armazenando as informações que deseja usar?

você pode apenas usar isso

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

e usá-lo

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

@adrianolsk obrigado pela explicação, mas estou usando angular não consigo importar o serviço no arquivo grapqh.module.ts Estou recebendo erros quando estou usando o serviço

Alguém pode saber como usar o serviço no arquivo module.ts sem usar a classe e o construtor

Obrigado

Estou tentando usar fromPromise para atualização assíncrona do token.
Basicamente seguindo a terceira caixa desta postagem

Estou obtendo e armazenando os tokens com sucesso, mas nenhum dos catch ou filter ou flatMap é chamado. Não tenho certeza de como depurar isso, então algumas sugestões serão úteis.

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 : essa abordagem parece sempre atualizar o token, mesmo antes de expirar, o que no caso de alguns serviços de autenticação (por exemplo, checkSession do Auth0 ) fará uma viagem de ida e volta desnecessária ao servidor Auth0 para cada solicitação GraphQL.

Estou tentando usar fromPromise para atualização assíncrona do token.
Basicamente seguindo a terceira caixa desta postagem

Estou obtendo e armazenando os tokens com sucesso, mas nenhum dos catch ou filter ou flatMap é chamado. Não tenho certeza de como depurar isso, então algumas sugestões serão úteis.

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

Eu descobri qual foi a causa do erro. Não visto no código acima, mas usei uma função map para mapear cada um dos erros resultantes. Isso fez com que onError não retornasse nada e o observável não fosse inscrito na operação de renovação de token.

Muito confuso e demorei muito para descobrir. Obrigado ao autor da postagem do blog por me ajudar.

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

@ WilsonLau0755 , tive o mesmo problema. Resolvido definindo todos os cabeçalhos null para uma string vazia '' .

Por que onError não está disponível apenas para uso com async await?

Esta página foi útil?
0 / 5 - 0 avaliações