<p>apollo-link-error - 如何异步刷新令牌?</p>

创建于 2018-06-15  ·  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回调不接受aync函数或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条评论

如果你对promise比较熟悉,可以使用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

进一步检查发现,在使用上述代码时,Apollo 链接的onError被调用了两次。 即使将refresh token承诺限制为运行一次,也不能修复错误。

发生的事情是:

1) 执行初始查询
2) 它失败并运行 apollo 的链接onError
3) ?? 它再次运行 apollo 的链接onError
4) Promise to refresh token, 在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

我的阿波罗链接堆栈现在如下:

[
   refreshTokenLink,
   requestLink,
   batchHttpLink
]

在执行对 graphql 端点的任何响应之前,始终调用refreshTokenLink来检查访问令牌,并且它的工作原理就像一个魅力。

不幸的是,这假设对 graphql 端点的调用必须始终经过身份验证(在我的情况下就是如此)。

看起来onError回调不接受aync函数或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 和 react native 我需要从 AsyncStorage onError 中删除一些令牌,所以它需要是一个异步函数

这个解决方案对我有用//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谢谢你的例子,它真的Observable subscriber是无效的返回值:它应该是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:我添加了一个关于这个的 PR: https :

这个问题在谷歌上排名很高,所以我在这里分享我的解决方案来帮助一些人: https :

我已经尝试过你的解决方案,但我面临着新的问题:

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 中)不会更新,这意味着对服务器的每个请求都将使用旧的访问令牌(随后将需要再次刷新)。

出于某种原因,每当我尝试在刷新期间更新 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。它是一个异步函数,返回一个 Promise):

  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 刷新令牌时遇到以下问题。 出于使用 nextjs 的 SSR 的目的,我正在从所有 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
我认为 ...headers 中的授权可能与新的授权冲突。

上面的问题是在 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)
  }
})

你好! 我使用完整的 websocket 传输,需要请求令牌查询。 不知道该怎么做。
当服务器响应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我们在上面发布了解决方法,看看 observables

我无法更新令牌任何人都可以提供工作示例

@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你能提供写的服务代码吗

import AuthService from 'services/auth-service' // 这是我的实现
const { 令牌 } = 等待 AuthService.getCredentials();

当我尝试导入服务时出现错误

那是我的服务,它只是从 react-native 读取 AsyncStorage,所以登录后我在那里设置了值,在每个请求之前,代码只是获取信息并设置在标题中,你可以这样做,或者如果你使用 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异步刷新令牌。
基本上遵循这篇文章的第三个框

我成功地获取和存储了令牌,但是catchfilterflatMap都没有被调用。 我不确定如何调试它,所以一些建议会有所帮助。

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 的checkSession )的情况下,这将为每个 GraphQL 请求进行不必要的 Auth0 服务器往返。

我正在尝试使用fromPromise异步刷新令牌。
基本上遵循这篇文章的第三个框

我成功地获取和存储了令牌,但是catchfilterflatMap都没有被调用。 我不确定如何调试它,所以一些建议会有所帮助。

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不返回任何内容,并且 observable 没有订阅令牌更新操作。

相当混乱,我花了很长时间才弄明白。 感谢博文的作者对我的帮助。

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

@WilsonLau0755 ,我遇到了同样的问题。 通过将所有null标头设置为空字符串''来解决它。

为什么 onError 不仅可用于 async await?

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

j3ddesign picture j3ddesign  ·  3评论

ignivalancy picture ignivalancy  ·  5评论

vjpr picture vjpr  ·  3评论

ash0080 picture ash0080  ·  4评论

ChenRoth picture ChenRoth  ·  4评论