<p>apollo-link-error - как асинхронно обновить токен?</p>

Созданный на 15 июн. 2018  ·  31Комментарии  ·  Источник: apollographql/apollo-link

Ярлыки выпуска

  • [] имеет репродукцию
  • [ ] особенность
  • [x] документы
  • [] блокировка <----- извините, не блокирую
  • [] хороший первый выпуск

Вопрос

Здесь упоминается точный сценарий, который я пытаюсь реализовать:

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

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

Однако, если срок действия токена истек, следует вызвать асинхронный resfreshToken и «подождать» до того, как getNewToken сможет вернуть действительный токен аутентификации. Я думаю.

У меня вопрос, как сделать асинхронный вызов resfreshToken . Я пробовал await refreshToken() (который разрешает обещание, когда оно завершено), но из моих записанных трассировок стека кажется, что это довольно сильно мешает RxJS. Я RxJS n00b, любая помощь приветствуется!

blocking docs

Самый полезный комментарий

Похоже, что onError callback не принимает aync function или Promise возвращает. См. Код https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74

Об этой проблеме ранее сообщалось: # 190

Я думаю, что было бы лучше, если бы apollo-link-error мог иметь дело с Promise , аналогично тому, что apollo-link-retry делает здесь: # 436

Все 31 Комментарий

Если вы больше знакомы с обещаниями, вы можете использовать fromPromise helper

import { fromPromise } from 'apollo-link';

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

@thymikee Пробовал ваше решение, и оно терпит неудачу со следующим сообщением:

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

Дальнейшая проверка показывает, что onError ссылки Apollo вызывается дважды при использовании приведенного выше кода. Даже ограничение однократного выполнения обещания refresh token не устраняет ошибку.

Что происходит:

1) Начальный запрос выполняется
2) Он терпит неудачу и запускает ссылку apollo onError
3) ?? Он снова запускает ссылку Apollo onError
4) Обещание обновить токен, в onError завершает выполнение и разрешается.
5) (Первоначальный запрос не выполняется второй раз после успешного выполнения обещания)
6) Первоначальный запрос возвращает результат, содержащий data как неопределенное

Надеюсь, что кто-то найдет решение этой проблемы, иначе нам придется вернуться к использованию долгоживущих токенов доступа, а не обновлять их по истечении срока действия.

Если ваша логика получения токена верна, onError следует вызывать только один раз. Похоже, у вас проблемы с запросом токена

@thymikee Выключил асинхронный запрос с фиктивным обещанием. По-прежнему возникает ошибка с указанным выше сообщением, и первоначальный запрос не запускается дважды. Все токены действительны на момент тестирования.

Код:

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

Изменить: удален fromPromise и он работает правильно. Каким-то образом обработка стека ссылок заканчивается до возврата результата, поэтому forward(operation) не выполняется.

После анализа кода fromPromise и фиксации # 172, fromPromise можно использовать только в предположениях с объектом Apollo Link.

Исследуя решение, я наконец наткнулся на этот проект: apollo-link-token-refresh

Мой стек ссылок apollo теперь выглядит следующим образом:

[
   refreshTokenLink,
   requestLink,
   batchHttpLink
]

refreshTokenLink всегда вызывается для проверки токена доступа перед выполнением любого ответа на конечную точку graphql и работает как шарм.

К сожалению, это предполагает, что вызов конечной точки graphql всегда должен быть аутентифицирован (что в моем случае так и есть).

Похоже, что onError callback не принимает aync function или Promise возвращает. См. Код https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74

Об этой проблеме ранее сообщалось: # 190

Я думаю, что было бы лучше, если бы apollo-link-error мог иметь дело с Promise , аналогично тому, что apollo-link-retry делает здесь: # 436

имея ту же проблему, используя apollo с реагирующим родным, мне нужно удалить некоторый токен из AsyncStorage onError, поэтому он должен быть асинхронной функцией

Это решение сработало для меня: https://stackoverflow.com/a/51321068/60223

Я решил это, создав утилиту 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
  });

а потом

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 спасибо за ваш пример, действительно помогает. Однако есть небольшая проблема: subscriber является недопустимым возвращаемым значением в соответствии с типами Observable : оно должно быть 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;
}

т.е. вместо этого безопасно вернуть undefined. Думаю, это следует упомянуть в файле README проекта.

UPD: Я добавил пиар по этому поводу: https://github.com/apollographql/apollo-link/pull/825

Эта проблема занимает высокое место в Google, поэтому я поделюсь своим решением здесь, чтобы помочь некоторым людям: https://gist.github.com/alfonmga/9602085094651c03cd2e270da9b2e3f7

Я пробовал ваше решение, но столкнулся с новой проблемой:

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'

Как вы, ребята, храните новый токен авторизации после его обновления?

Конечно, я могу установить новые заголовки в запросе на повторную попытку, однако исходный токен доступа (который я храню в файлах cookie) не обновляется, что означает, что каждый отдельный запрос к серверу будет использовать старый токен доступа (и впоследствии нужно будет обновить еще раз).

По какой - то причине , я получаю следующее сообщение об ошибке , когда я пытаюсь обновить печенье во время обновления (я создал новый вопрос здесь об этом):

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 может быть , это поможет вам https://stackoverflow.com/questions/55356736/change-apollo-client-options-for-jwt-token я бегу в аналогичный вопрос о том , как обновить маркер

Привет, спасибо @ crazy4groovy. Я пробовал ваше решение, но у меня все еще есть проблема, что промежуточное ПО, в которое я добавляю токен в запрос graphql, вызывается до того, как новый токен будет установлен в запрос. Следовательно, в заголовке все еще есть недопустимый токен.

Немного справочной информации: мы получаем сетевую ошибку, когда токен недействителен, и с помощью токена обновления мы можем получить новый и повторить попытку. Но поскольку промежуточное ПО вызывается до того, как токен обновления будет собран и установлен в локальное хранилище, он все еще имеет недопустимый токен. Логика обновления токена работает нормально, поскольку в конце мы получаем новый набор токенов. Я немного отладил проблему, и сроки следующие:

  • Промежуточное ПО: прикреплен недопустимый токен
  • Запрос на запрос сделан
  • Сетевая ошибка: 401, потому что токен недействителен, перехватывается и в onError обрабатывается с помощью логики promiseToObservable и повторяется.
  • Пока мы не закончим получение токена в обещании onRefreshToken , промежуточное ПО уже находится во втором запуске со старым токеном.
  • Токен обновлен в локальном хранилище ...

Вот фрагмент этих частей (пропуская onRefreshtoken. Это асинхронная функция, возвращающая обещание):

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

Не могли бы вы сказать мне, что мне здесь не хватает? Заранее большое спасибо...

ОК, извините за путаницу, если есть ...

Я нашел ответ, и это глупо после того, как я искал его часами и, конечно же, после публикации здесь: во время инициализации клиента apollo я поменял местами промежуточное ПО и ссылку на ошибку. Теперь это работает. Ссылка на ошибку должна быть первой, очевидно ..
старый: link: from([authMiddleware, errorLink, /* others */])
новое: link: from([errorLink, authMiddleware, /* others */])

Прости еще раз..

Привет ребята,

У меня возникла следующая проблема с использованием onError для токенов обновления. Для SSR с использованием nextjs я собираю данные из всех запросов graphql, но что происходит, например, когда у нас есть 2 запроса, и каждый из них заканчивается ошибкой, потому что токен jwt истек. Затем он запускает дважды onError, и мы дважды вызываем токены обновления, что дорого. Я не могу понять, откуда могла взяться проблема. Вот код, который я использую. Не могли бы вы помочь с этим.

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

Я немного боролся с этой проблемой, но, наконец, у меня все получилось. Я скинул пакет.

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

Основное различие между этим пакетом и пакетом, называемым apollo-link-token-refresh, заключается в том, что этот пакет будет ожидать сетевой ошибки перед попыткой обновления.

Дайте мне знать, если у вас есть идеи по поводу изменений.

Вот базовое использование:

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

Я решил это, создав утилиту 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
  });

а потом

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

Я использую его и обнаружил, что он все еще использует старый токен после запроса на обновление токена. Итак, я пытаюсь сделать следующее:

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

и это работает.
Однако у него все еще есть проблема, что если я добавлю ...headers (означает повторное добавление старых заголовков), что-то не так до отправки прямого запроса:
ERROR Error: Network error: Cannot read property 'length' of null
Я думаю, что заголовки «Авторизация в ...» могут конфликтовать с новой авторизацией.

проблема выше находится в apollo-angular "apollo-angular-link-http": "^1.6.0", а не в apollo-client "apollo-link-http": "^1.5.16", то время как ошибка ссылки такая же "apollo-link-error": "^1.1.12",

другой синтаксис: глаза:

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

Привет! Я использую полный транспорт веб-сокетов, нужно запросить токен. Не знаю, как это сделать.
Я хочу сделать запрос на получение, когда сервер отвечает, что срок действия accessToken истек.

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, мы опубликовали обходные пути выше, взгляните на наблюдаемые

Я не могу обновить токен, может ли кто-нибудь предоставить рабочий пример

@ Ramyapriya24 вот код, который я использую.

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, можете ли вы предоставить сервисный код, написанный

импортировать AuthService из 'services / auth-service' // это моя реализация
const {токен} = ждать AuthService.getCredentials ();

когда я пытаюсь импортировать сервис, я получаю ошибки

Это моя служба, она просто считывает AsyncStorage из response-native, поэтому после входа в систему я устанавливаю значение там, и перед каждым запросом код просто захватывает информацию и задает ее в заголовке, вы можете сделать то же самое, или используя localStorage, если вы находятся в сети.

Где вы храните информацию, которую хотите использовать?

ты можешь просто использовать это

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

и использовать это

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

@adrianolsk, спасибо за объяснение, но я использую angular. Я не могу импортировать сервис в файл grapqh.module.ts. У меня возникают ошибки, когда я использую сервис.

может ли кто-нибудь знать, как использовать службу в файле module.ts без использования класса и конструктора

Спасибо

Я пытаюсь использовать fromPromise для асинхронного обновления токена.
В основном следуя третьему блоку из этого сообщения

Я успешно получаю и сохраняю токены, но ни catch ни filter ни flatMap не вызываются. Я не знаю, как это отладить, поэтому некоторые предложения будут мне полезны.

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 : этот подход, кажется, всегда обновляет токен, даже до того, как он истечет, что в случае некоторых служб аутентификации (например, Auth0's checkSession ) сделает ненужный обход сервера Auth0 для каждого запроса GraphQL.

Я пытаюсь использовать fromPromise для асинхронного обновления токена.
В основном следуя третьему блоку из этого сообщения

Я успешно получаю и сохраняю токены, но ни catch ни filter ни flatMap не вызываются. Я не знаю, как это отладить, поэтому некоторые предложения будут мне полезны.

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

Я нашел причину ошибки. Не видно в приведенном выше коде, но я использовал функцию map для сопоставления каждой из возникающих ошибок. Это привело к тому, что onError ничего не вернуло, а наблюдаемый объект не был подписан на операцию обновления токена.

Довольно запутанно, и мне потребовалось так много времени, чтобы понять это. Спасибо автору сообщения в блоге за помощь.

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

@ WilsonLau0755 , у меня была такая же проблема. Решил это, установив все заголовки null в пустую строку '' .

Почему onError нельзя использовать только с async await?

Была ли эта страница полезной?
0 / 5 - 0 рейтинги