<p>apollo-link-error - bagaimana cara async menyegarkan token?</p>

Dibuat pada 15 Jun 2018  ·  31Komentar  ·  Sumber: apollographql/apollo-link

Label Masalah

  • [ ] memiliki-reproduksi
  • [ ] fitur
  • [x] dokumen
  • [ ] memblokir <----- maaf, tidak memblokir
  • [ ] edisi pertama yang bagus

Pertanyaan

Skenario tepat yang saya coba capai disebutkan di sini:

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

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

Namun, jika token kedaluwarsa, async resfreshToken harus dipanggil dan "menunggu", sebelum getNewToken dapat mengembalikan token autentikasi yang valid. Kupikir.

Pertanyaan saya adalah, bagaimana melakukan panggilan async resfreshToken . Saya sudah mencoba await refreshToken() (yang menyelesaikan janji ketika selesai), tetapi dari jejak tumpukan yang saya log tampaknya ini cukup banyak mengacaukan RxJS. Saya seorang RxJS n00b, bantuan apa pun sangat dihargai!

blocking docs

Komentar yang paling membantu

Sepertinya onError callback tidak menerima fungsi aync atau Promise kembali. Lihat kode https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74

Masalah ini telah dilaporkan sebelumnya: #190

Saya pikir itu akan bekerja lebih baik jika apollo-link-error dapat menangani Promise , mirip dengan apa yang dilakukan apollo-link-retry sini: #436

Semua 31 komentar

Jika Anda lebih akrab dengan janji, Anda dapat menggunakan fromPromise helper

import { fromPromise } from 'apollo-link';

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

@thymikee Mencoba solusi Anda dan gagal dengan pesan berikut:

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

Pemeriksaan lebih lanjut menunjukkan bahwa tautan Apollo onError dipanggil dua kali saat menggunakan kode di atas. Bahkan membatasi janji refresh token untuk dijalankan sekali, tidak memperbaiki kesalahan.

Apa yang terjadi adalah:

1) Permintaan awal dijalankan
2) Gagal dan menjalankan tautan apollo onError
3) ?? Ini menjalankan tautan apollo onError lagi
4) Janji untuk menyegarkan token, di onError selesai dieksekusi dan diselesaikan.
5) (Kueri awal tidak dieksekusi untuk kedua kalinya setelah janji berhasil)
6) Permintaan awal mengembalikan hasil yang berisi data sebagai tidak terdefinisi

Ini untuk berharap seseorang menemukan solusi untuk ini, jika tidak, kita harus kembali menggunakan token akses yang berumur panjang daripada menyegarkannya setelah kedaluwarsa.

Jika logika pengambilan token Anda benar, onError hanya boleh dipanggil sekali. Sepertinya Anda memiliki masalah dengan kueri token Anda

@thymikee Mengganti permintaan asinkron dengan janji palsu. Masih gagal dengan pesan di atas dan kueri awal tidak dijalankan dua kali. Semua token valid pada saat pengujian.

Kode:

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

Sunting: Menghapus fromPromise dan berfungsi dengan benar. Entah bagaimana, pemrosesan tumpukan tautan berakhir sebelum mengembalikan hasilnya sehingga forward(operation) tidak dieksekusi.

Setelah menganalisis kode fromPromise dan commit #172 , fromPromise hanya dapat digunakan dalam dugaan dengan objek Apollo Link.

Setelah meneliti solusi, saya akhirnya menemukan proyek ini: apollo-link-token-refresh

Tumpukan tautan apollo saya sekarang sebagai berikut:

[
   refreshTokenLink,
   requestLink,
   batchHttpLink
]

refreshTokenLink selalu dipanggil untuk memeriksa token akses sebelum mengeksekusi respons apa pun ke titik akhir graphql dan berfungsi seperti pesona.

Sayangnya, ini mengasumsikan bahwa panggilan ke titik akhir graphql harus selalu diautentikasi (yang, dalam kasus saya).

Sepertinya onError callback tidak menerima fungsi aync atau Promise kembali. Lihat kode https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74

Masalah ini telah dilaporkan sebelumnya: #190

Saya pikir itu akan bekerja lebih baik jika apollo-link-error dapat menangani Promise , mirip dengan apa yang dilakukan apollo-link-retry sini: #436

memiliki masalah yang sama, menggunakan apollo dengan reaksi asli saya perlu menghapus beberapa token dari AsyncStorage onError sehingga perlu fungsi async

Solusi ini bekerja untuk saya: https://stackoverflow.com/a/51321068/60223

Saya memecahkan ini dengan membuat utilitas 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
  });

lalu

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 terima kasih atas contoh Anda, ini sangat membantu. Namun, ada masalah kecil dengannya: subscriber adalah nilai pengembalian yang tidak valid menurut pengetikan Observable : seharusnya 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;
}

yaitu aman untuk mengembalikan tidak terdefinisi sebagai gantinya. Saya kira itu harus disebutkan dalam file README proyek.

UPD: Saya menambahkan PR tentang ini: https://github.com/apollographql/apollo-link/pull/825

Masalah ini berperingkat tinggi di Google jadi saya membagikan solusi saya di sini untuk membantu beberapa orang: https://Gist.github.com/alfonmga/9602085094651c03cd2e270da9b2e3f7

Saya telah mencoba solusi Anda tetapi saya menghadapi masalah baru:

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'

Bagaimana kalian menyimpan token auth baru setelah di-refresh?

Tentu, saya dapat mengatur tajuk baru dalam permintaan coba lagi, namun token akses asli (yang saya simpan di cookie) tidak diperbarui yang berarti bahwa setiap permintaan ke server akan menggunakan token akses lama (dan selanjutnya perlu disegarkan lagi).

Untuk beberapa alasan saya mendapatkan pesan kesalahan berikut setiap kali saya mencoba memperbarui cookie selama penyegaran (saya membuat masalah baru di sini tentang itu):

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 mungkin ini akan membantu Anda https://stackoverflow.com/questions/55356736/change-apollo-client-options-for-jwt-token Saya mengalami masalah serupa tentang cara memperbarui token

Hai, terima kasih @crazy4groovy. Saya sudah mencoba solusi Anda tetapi saya masih mengalami masalah, bahwa middleware tempat saya menambahkan token ke permintaan graphql dipanggil sebelum token baru disetel ke permintaan. Oleh karena itu, header masih memiliki token yang tidak valid.

Sedikit info latar belakang: Kami mendapatkan kesalahan jaringan, ketika token tidak valid, dan melalui token penyegaran, kami bisa mendapatkan yang baru dan mencoba lagi. Tetapi karena middleware dipanggil sebelum token penyegaran dikumpulkan dan disetel ke penyimpanan lokal, itu masih memiliki yang tidak valid. Segarkan logika token berfungsi dengan baik, karena kami kemudian mendapatkan set token baru pada akhirnya. Saya telah sedikit men-debug masalah dan waktunya adalah sebagai berikut:

  • Middleware: token tidak valid terpasang
  • Permintaan permintaan dibuat
  • Kesalahan jaringan: 401, karena token tidak valid, ditangkap dan di onError ditangani melalui logika promiseToObservable dan dicoba lagi.
  • Sampai kita selesai mendapatkan token dalam janji onRefreshToken , middleware sudah di jalankan kedua dengan token lama.
  • Token diperbarui di penyimpanan lokal...

Berikut cuplikan dari bagian-bagian ini (melewatkan onRefreshtoken. Ini adalah fungsi asinkron, mengembalikan Janji):

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

Bisakah Anda memberi tahu saya apa yang saya lewatkan di sini? Terima kasih banyak sebelumnya...

Oke, maaf atas kebingungan, jika ada ...

Saya telah menemukan jawabannya dan itu adalah jawaban yang bodoh setelah mencari selama berjam-jam dan menemukan - tentu saja - setelah memposting di sini: selama inisialisasi klien apollo, saya telah menukar middleware dan tautan kesalahan. Sekarang berhasil. Link error harus yang pertama, jelas..
lama: link: from([authMiddleware, errorLink, /* others */])
baru: link: from([errorLink, authMiddleware, /* others */])

Maaf lagi..

Hallo teman-teman,

Saya memiliki masalah berikut menggunakan onError untuk token penyegaran. Untuk tujuan SSR menggunakan nextjs saya mengumpulkan data dari semua kueri graphql, tetapi apa yang terjadi ketika kami memiliki 2 kueri misalnya dan masing-masing berakhir dengan kesalahan karena token jwt kedaluwarsa. Kemudian menyalakan dua kali onError dan kami memanggil dua kali untuk token penyegaran yang mahal. Saya tidak tahu dari mana masalah itu mungkin berasal. Berikut adalah kode yang saya gunakan. Bisakah Anda membantu dengan ini.

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

Saya bergulat dengan masalah ini sebentar, tetapi akhirnya saya berhasil. Aku melemparkan sebuah paket bersama-sama.

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

Perbedaan utama antara paket ini dan yang disebut apollo-link-token-refresh adalah bahwa paket ini akan menunggu kesalahan jaringan sebelum mencoba melakukan penyegaran.

Beri tahu saya jika Anda memiliki ide untuk perubahan.

Berikut penggunaan dasarnya:

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

Saya memecahkan ini dengan membuat utilitas 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
  });

lalu

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

Saya menggunakannya dan ternyata masih menggunakan token lama setelah permintaan token penyegaran. jadi, saya mencoba sebagai berikut:

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

dan itu bekerja.
Namun, masih ada masalah jika saya menambahkan ...headers (artinya menambahkan kembali header lama), ada yang salah sebelum permintaan penerusan dikirim:
ERROR Error: Network error: Cannot read property 'length' of null
Saya pikir Otorisasi di ...header mungkin bertentangan dengan Otorisasi baru.

masalah di atas ada di apollo-angular "apollo-angular-link-http": "^1.6.0", dan bukan di apollo-client "apollo-link-http": "^1.5.16", sedangkan link-error sama "apollo-link-error": "^1.1.12",

sintaks lain :mata:

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

Hai! Saya menggunakan transportasi soket web lengkap, perlu meminta kueri token. Tidak tahu bagaimana melakukan itu.
Saya ingin melakukan permintaan terima ketika server menjawab bahwa accessToken telah kedaluwarsa.

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 kami memposting solusi di atas, lihat yang dapat diamati

Saya tidak dapat memperbarui token, adakah yang bisa memberikan contoh kerja?

@Ramyapriya24 di sini adalah kode yang saya gunakan.

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 dapatkah Anda memberikan kode layanannya?

import AuthService dari 'services/auth-service' // ini implementasi saya
const { token } = menunggu AuthService.getCredentials();

ketika saya mencoba mengimpor layanan, saya mendapatkan kesalahan

Itu adalah layanan saya, itu hanya membaca AsyncStorage dari reaksi asli, jadi setelah login saya menetapkan nilai di sana dan sebelum setiap permintaan kode ambil saja info dan atur di header, Anda bisa melakukan hal yang sama, atau menggunakan localStorage jika Anda ada di web.

Di mana Anda menyimpan informasi yang ingin Anda gunakan?

Anda hanya dapat menggunakan ini

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

dan gunakan itu

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

@adrianolsk terima kasih atas penjelasannya tetapi saya menggunakan angular Saya tidak dapat mengimpor layanan dalam file grapqh.module.ts Saya mendapatkan kesalahan ketika saya menggunakan layanan ini

adakah yang bisa tahu cara menggunakan layanan dalam file module.ts tanpa menggunakan kelas dan konstruktor?

Terima kasih

Saya mencoba menggunakan fromPromise untuk async menyegarkan token.
Pada dasarnya mengikuti kotak ketiga dari posting ini

Saya berhasil mendapatkan dan menyimpan token, tetapi catch atau filter atau flatMap dipanggil. Saya tidak yakin bagaimana men-debug ini, jadi beberapa saran akan sangat membantu.

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 : pendekatan itu tampaknya selalu menyegarkan token, bahkan sebelum kedaluwarsa, yang dalam kasus beberapa layanan otentikasi (mis. Auth0's checkSession ) akan membuat perjalanan bolak-balik server Auth0 yang tidak perlu untuk setiap permintaan GraphQL.

Saya mencoba menggunakan fromPromise untuk async menyegarkan token.
Pada dasarnya mengikuti kotak ketiga dari posting ini

Saya berhasil mendapatkan dan menyimpan token, tetapi catch atau filter atau flatMap dipanggil. Saya tidak yakin bagaimana men-debug ini, jadi beberapa saran akan sangat membantu.

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

Saya telah menemukan apa penyebab kesalahan tersebut. Tidak terlihat pada kode di atas, tetapi saya menggunakan fungsi map untuk memetakan setiap kesalahan yang dihasilkan. Ini menyebabkan onError tidak mengembalikan apa pun dan observable tidak berlangganan operasi untuk pembaruan token.

Cukup membingungkan dan butuh waktu lama bagi saya untuk memahaminya. Terima kasih kepada penulis posting blog karena telah membantu saya.

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

@WilsonLau0755 , saya memiliki masalah yang sama. Memecahkannya dengan mengatur semua null header ke string kosong '' .

Mengapa onError tidak hanya tersedia untuk digunakan dengan async menunggu?

Apakah halaman ini membantu?
0 / 5 - 0 peringkat