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

κ·ΈλŸ¬λ‚˜ 토큰이 만료되면 getNewToken κ°€ μœ νš¨ν•œ 인증 토큰을 λ°˜ν™˜ν•  수 있기 전에 비동기 resfreshToken λ₯Ό ν˜ΈμΆœν•˜κ³  "λŒ€κΈ°"ν•΄μ•Ό ν•©λ‹ˆλ‹€. 제 μƒκ°μ—λŠ”.

λ‚΄ μ§ˆλ¬Έμ€ 비동기 resfreshToken ν˜ΈμΆœμ„ μˆ˜ν–‰ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€. await refreshToken() (Promiseκ°€ μ™„λ£Œλ˜λ©΄ ν•΄κ²°)λ₯Ό μ‹œλ„ν–ˆμ§€λ§Œ 기둝된 μŠ€νƒ μΆ”μ μ—μ„œ 이것이 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-retry κ°€ μ—¬κΈ°μ„œ ν•˜λŠ” 것과 μœ μ‚¬ν•˜κ²Œ apollo-link-error κ°€ Promise μ²˜λ¦¬ν•  수 있으면 더 잘 μž‘λ™ν•  것이라고 μƒκ°ν•©λ‹ˆλ‹€. #436

λͺ¨λ“  31 λŒ“κΈ€

Promise에 더 μ΅μˆ™ν•˜λ‹€λ©΄ fromPromise λ„μš°λ―Έλ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

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
μ‚Ό) ?? 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

λ‚΄ 폴둜 링크 μŠ€νƒμ€ 이제 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

[
   refreshTokenLink,
   requestLink,
   batchHttpLink
]

refreshTokenLink λŠ” 항상 graphql 끝점에 λŒ€ν•œ 응닡을 μ‹€ν–‰ν•˜κΈ° 전에 μ•‘μ„ΈμŠ€ 토큰을 ν™•μΈν•˜κΈ° μœ„ν•΄ 호좜되며 맀λ ₯처럼 μž‘λ™ν•©λ‹ˆλ‹€.

λΆˆν–‰νžˆλ„ 이것은 graphql 끝점에 λŒ€ν•œ 호좜이 항상 μΈμ¦λ˜μ–΄μ•Ό ν•œλ‹€κ³  κ°€μ •ν•©λ‹ˆλ‹€(제 κ²½μš°μ—λŠ” 인증됨).

onError 콜백이 aync ν•¨μˆ˜ λ˜λŠ” Promise λ°˜ν™˜μ„ ν—ˆμš©ν•˜μ§€ μ•ŠλŠ” 것 κ°™μŠ΅λ‹ˆλ‹€. μ½”λ“œ μ°Έμ‘° https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60 -L74

이 λ¬Έμ œλŠ” 이전에 λ³΄κ³ λ˜μ—ˆμŠ΅λ‹ˆλ‹€: #190

apollo-link-retry κ°€ μ—¬κΈ°μ„œ ν•˜λŠ” 것과 μœ μ‚¬ν•˜κ²Œ apollo-link-error κ°€ Promise μ²˜λ¦¬ν•  수 있으면 더 잘 μž‘λ™ν•  것이라고 μƒκ°ν•©λ‹ˆλ‹€. #436

λ™μΌν•œ λ¬Έμ œκ°€ 있고 λ°˜μ‘ λ„€μ΄ν‹°λΈŒμ™€ ν•¨κ»˜ apolloλ₯Ό μ‚¬μš©ν•˜μ—¬ AsyncStorage onErrorμ—μ„œ 일뢀 토큰을 μ œκ±°ν•΄μ•Ό 비동기 ν•¨μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€.

이 μ†”λ£¨μ…˜μ€ μ €μ—κ²Œ 효과적 μ΄μ—ˆμŠ΅λ‹ˆλ‹€:

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 typingsκ°€ : μ˜€νžˆλ €ν•΄μ•Ό 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://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'

μƒˆ 인증 토큰이 μƒˆλ‘œ 고쳐지면 μ–΄λ–»κ²Œ μ €μž₯ν•˜κ³  κ³„μ‹­λ‹ˆκΉŒ?

λ¬Όλ‘  μž¬μ‹œλ„ μš”μ²­μ—μ„œ μƒˆ 헀더λ₯Ό μ„€μ •ν•  수 μžˆμ§€λ§Œ μ›λž˜ μ•‘μ„ΈμŠ€ 토큰(쿠킀에 μ €μž₯ν•˜κ³  있음)은 μ—…λ°μ΄νŠΈλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μƒˆλ‘œκ³ μΉ¨ν•΄μ•Ό 함).

μ–΄λ–€ 이유둜 μƒˆλ‘œ κ³ μΉ¨ 쀑에 μΏ ν‚€λ₯Ό μ—…λ°μ΄νŠΈν•˜λ €κ³  ν•  λ•Œλ§ˆλ‹€ λ‹€μŒ 였λ₯˜ λ©”μ‹œμ§€κ°€ λ‚˜νƒ€λ‚©λ‹ˆλ‹€( μ—¬κΈ°μ—μ„œ 이에 λŒ€ν•΄ μƒˆ 문제λ₯Ό λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€).

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, 토큰이 μœ νš¨ν•˜μ§€ μ•Šμ•„ catch되고 onError μ—μ„œ promiseToObservable 논리λ₯Ό 톡해 처리되고 λ‹€μ‹œ μ‹œλ„λ©λ‹ˆλ‹€.
  • onRefreshToken Promiseμ—μ„œ 토큰 κ°€μ Έμ˜€κΈ°κ°€ μ™„λ£Œλ  λ•ŒκΉŒμ§€ λ―Έλ“€μ›¨μ–΄λŠ” 이미 이전 ν† ν°μœΌλ‘œ 두 번째 μ‹€ν–‰ μ€‘μž…λ‹ˆλ‹€.
  • 토큰은 둜컬 μ €μž₯μ†Œμ—μ„œ μ—…λ°μ΄νŠΈλ©λ‹ˆλ‹€...

λ‹€μŒμ€ μ΄λŸ¬ν•œ λΆ€λΆ„μ˜ μŠ€λ‹ˆνŽ«μž…λ‹ˆλ‹€(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
... ν—€λ”μ˜ κΆŒν•œ λΆ€μ—¬κ°€ μƒˆ κΆŒν•œ 뢀여와 μΆ©λŒν•  수 μžˆλ‹€κ³  μƒκ°ν•©λ‹ˆλ‹€.

μœ„μ˜ λ¬Έμ œλŠ” apollo-angular "apollo-angular-link-http": "^1.6.0", 이고 apollo-client "apollo-link-http": "^1.5.16", μ•„λ‹Œ 반면 link-errorλŠ” "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 μž‘μ„±λœ μ„œλΉ„μŠ€ μ½”λ“œλ₯Ό μ œκ³΅ν•  수 μžˆμŠ΅λ‹ˆκΉŒ?

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 μ„€λͺ… κ°μ‚¬ν•©λ‹ˆλ‹€λ§Œ 각도λ₯Ό μ‚¬μš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. 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의 checkSession )의 경우 λͺ¨λ“  GraphQL μš”μ²­μ— λŒ€ν•΄ λΆˆν•„μš”ν•œ Auth0 μ„œλ²„ 왕볡을 λ§Œλ“­λ‹ˆλ‹€.

비동기 μƒˆλ‘œ κ³ μΉ¨ 토큰에 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 λ“±κΈ‰