Apollo-link: apollo-link-error์˜ networkError์—๋Š” ์ƒํƒœ ์†์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค.

์— ๋งŒ๋“  2017๋…„ 12์›” 02์ผ  ยท  35์ฝ”๋ฉ˜ํŠธ  ยท  ์ถœ์ฒ˜: apollographql/apollo-link

์ฃผ์ œ๋กœ. ๊ฐ์‚ฌ ํ•ด์š”

const errorLink = onError(({ networkError }) => {
  if (networkError.status === 401) {
    logout();
  }
})

screen shot 2017-12-03 at 4 49 20 am

๊ฐ€์žฅ ์œ ์šฉํ•œ ๋Œ“๊ธ€

๊ทธ๋ž˜์„œ ์•„์ง๋„ ์ฐพ๊ณ  ์žˆ๋Š” ๋ˆ„๊ตฐ๊ฐ€๋ฅผ ์œ„ํ•ด...

statusCode ๊ฐ’์€ networkError์˜ ์†์„ฑ์œผ๋กœ ๋ฐ˜ํ™˜๋˜๊ณ  networkError์— ๋Œ€ํ•œ ์—…๋ฐ์ดํŠธ๋œ ์œ ํ˜•์ด ์—ฌ๊ธฐ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. https://github.com/apollographql/apollo-link/pull/530

networkError ๋Š” ์ด์ œ Error | ServerError | ServerParseError ์˜ ๊ณต์šฉ์ฒด ์œ ํ˜•์ด๋ฏ€๋กœ Typescript๋Š” ๊ณต์šฉ์ฒด์˜ ๋ชจ๋“  ์œ ํ˜•์— ๊ณตํ†ต์ ์ธ ์†์„ฑ์—๋งŒ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. statusCode ๋Š” Error ์œ ํ˜•์— ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Typescript๊ฐ€ ๋ถˆํ‰ํ•˜์ง€ ์•Š๊ณ ๋Š” networkError.statusCode ๋ฅผ ํ†ตํ•ด ์•ก์„ธ์Šคํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์œ ํ˜•์„ ๊ตฌ๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด ์œ ํ˜• ๊ฐ€๋“œ ๋˜๋Š” ๋‹ค๋ฅธ ๊ฒƒ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Typescript ํ•ธ๋“œ๋ถ์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ช‡ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์ œ์•ˆ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. https://www.typescriptlang.org/docs/handbook/advanced-types.html#type -guards-and-differentiating-types

๋‚ด ๋ชฉ์ ์„ ์œ„ํ•ด in ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

  onError: ({ networkError }: ErrorResponse) => {
    if (
      networkError &&
      'statusCode' in networkError &&
      networkError.statusCode === 401
    ) {
      // perform logout stuff
    }
  },

@JoviDeCroock ์ด ๋ฌธ์ œ๋ฅผ ๋‹ซ์•„๋„ ์•ˆ์ „ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•˜์‹ญ๋‹ˆ๊นŒ?

๋ชจ๋“  35 ๋Œ“๊ธ€

๋‚ด๊ฐ€ ์•„๋Š” ๋ฐ”์™€ ๊ฐ™์ด "failed to fetch"๋Š” ์‘๋‹ต์ด ์—†์œผ๋ฉด(์˜ˆ: ์„œ๋ฒ„๊ฐ€ ๋‹ค์šด๋œ ๊ฒฝ์šฐ) ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ ์ฝ”๋“œ๊ฐ€ ํ‘œ์‹œ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋‚ด๊ฐ€ ํ‹€๋ ธ๋‹ค๋ฉด ์ €๋ฅผ ์ˆ˜์ •ํ•˜์‹ญ์‹œ์˜ค.

๊ทธ๋Ÿฌ๋‚˜ ๊ทธ๋Ÿฐ ์ƒํ™ฉ์—์„œ ๋ฌธ์ž ๋ฉ”์‹œ์ง€๋งŒ ๊ฐ–๋Š” ๊ฒƒ์€ ์ตœ์„ ์˜ ์•„์ด๋””์–ด๊ฐ€ ์•„๋‹ˆ๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜ ์ฝ”๋“œ์™€ ๊ฐ™์ด ๋” ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌด์–ธ๊ฐ€๊ฐ€ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋‚˜๋Š” ์š”์ฒญ์ด ํฌ๋กฌ ์ธ์ŠคํŽ™ํ„ฐ์˜ ๋„คํŠธ์›Œํฌ ํƒœ๊ทธ์—์„œ (401)๋กœ ๋ณด๋‚ด๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ๋ณด์•˜์Šต๋‹ˆ๋‹ค.

#218๊ณผ ๊ด€๋ จ์ด ์žˆ๋Š” ๊ฒƒ ๊ฐ™์•„์š”(ํ™•์‹คํ•˜์ง€ ์•Š์Œ..)

๋˜ํ•œ์ด ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. networkError.status ๋˜๋Š” networkError.statusCode ์ด(๊ฐ€) ์—†๊ณ  ์‘๋‹ต ๊ฐœ์ฒด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ๊ณ„์ธต์—์„œ HTTP ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋ฐฉ๋ฒ•์ด ์—†์Šต๋‹ˆ๋‹ค.

๋‚˜๋Š” ๋˜ํ•œ ํ˜„์žฌ์ด ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ตœ์‹  ๋ฆด๋ฆฌ์Šค์—์„œ ์ˆ˜์ •๋œ ๊ฒƒ์œผ๋กœ ์ถ”์ •๋˜์ง€๋งŒ ์—ฌ์ „ํžˆ ์ด ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

@jbaxleyiii ์— ๋Œ€ํ•œ ์ƒ๊ฐ์ด

@akrigline ๊ณผ @bogdansoare ๊ฐ€ ์—ฌ์ „ํžˆ ๋ฌธ์ œ๋ฅผ ๊ฒช๊ณ  ์žˆ๋‹ค๋Š” ์†Œ์‹์„ ๋“ค์œผ๋‹ˆ ์œ ๊ฐ์ž…๋‹ˆ๋‹ค! ์ด๊ฒƒ์€ ํ™•์‹คํžˆ ๊ณ ์ณ์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋‚˜๋Š” ์ง€๊ธˆ #364๋ฅผ ํ†ตํ•ด ์ผํ•˜๊ณ  ์žˆ๋‹ค. ๊ทธ๋™์•ˆ ์ด ์ฝ”๋“œ ์ƒŒ๋“œ๋ณด์Šค ๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•˜์—ฌ ์˜ค๋ฅ˜๋ฅผ ์žฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ? ์•„๋‹ˆ๋ฉด ์‹คํŒจํ•œ ํ…Œ์ŠคํŠธ๋กœ PR์„ ์—ฌ๋Š” ๊ฒƒ์ด ๋” ๋‚ซ์Šต๋‹ˆ๊นŒ?

์–ด๋–ค ์†Œ์‹?

๋˜ํ•œ ์ด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์‘๋‹ต ๊ฐ์ฒด๊ฐ€ ๋น„์–ด ์žˆ๊ณ  networkError ๊ฐ์ฒด์— statusCode๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

networkError ์†์„ฑ๋„ ์–ป์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.
screen shot 2018-05-07 at 13 01 56

ํŽธ์ง‘: ์•„๋งˆ๋„ #475

Error ์œ ํ˜•์„ ๋ฌด์‹œํ•˜์—ฌ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ์ง€๋งŒ ์ด๊ฒƒ์€ ์™„์ „ํžˆ ์ž„์‹œ์ ์ธ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์ด๋ฉฐ ํ–ฅํ›„ ์ˆ˜์ • ์‚ฌํ•ญ์„ ๊ธฐ๋‹ค๋ ค์•ผ ํ•ฉ๋‹ˆ๋‹ค. @์—๋ฐ˜์Šค

์—ฌ๊ธฐ ์ด ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค!

์šฐ๋ฆฌ๊ฐ€ ๊ทธ๋“ค์— ๋Œ€ํ•ด ์“ด networkError ์˜ ํ˜„์žฌ๋Š” ์‹ค์ œ ์„œ๋ฒ„ ์ธก ์˜ค๋ฅ˜๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ์š”์•ฝํ•˜๋ฉด ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ์—๋Š” statusCode ๊ฐ€ ์ „ํ˜€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฉฐ ์‹ค์ œ๋กœ graphql ์„œ๋ฒ„์—์„œ ์‹ค์ œ ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ์ˆ˜์‹ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (_์„œ๋ฒ„์™€์˜ ํ†ต์‹  ๋ฌธ์ œ๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ statusCode _์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.)

screen shot 2018-05-07 at 20 38 09

์„œ์ฒด ์˜ค๋ฅ˜๊ฐ€ ์•„์ง ๋‚จ์•„ ์žˆ์Šต๋‹ˆ๋‹ค!

TypeScript๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ networkError ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์œ ํ˜•์ด ์žˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

export interface ErrorResponse {
  graphQLErrors?: GraphQLError[];
  networkError?: Error;
  response?: ExecutionResult;
  operation: Operation;
}

๊ทธ๋ฆฌ๊ณ  Error ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์˜๋ฉ๋‹ˆ๋‹ค.

interface Error {
    name: string;
    message: string;
    stack?: string;
}

์—ฌ๊ธฐ์„œ statusCode ๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์ง€ ์•Š์Œ์„ ์•Œ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ TypeScript์—์„œ networkError.statusCode ์™€ ๊ฐ™์€ ๊ฒƒ์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

๋‚˜์˜ ์ž„์‹œ ํ•ด๊ฒฐ์ฑ…

networkError ๋ฅผ any ๋กœ ์บ์ŠคํŠธํ•ด์•ผ ํ•˜๋ฉฐ ๋˜ํ•œ ์ด๋ฅผ ๋นˆ object ๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

const afterWareLink = onError(({operation, response, graphQLErrors = {}, networkError = {} as any}) => {
    const status: number = networkError && networkError.statusCode ? networkError.statusCode : null;
    debugHelper.error('apolloError', {
        operation,
        response,
        graphQLErrors,
        networkError,
        status,
    });

    // Do your job
    // if (status && HTTPExceptions[status])
    //     redirectService.redirectWithReload(`/error/${status}`);
});

์œ„์˜ afterWareLink ์ฝ”๋“œ์— ๋”ฐ๋ฅด๋ฉด 400 ์ƒํƒœ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์œผ๋ฉฐ ์ด์ œ ์ด ์ƒํƒœ ์ฝ”๋“œ์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

screen shot 2018-05-07 at 20 28 03

๋‹ค์Œ์€ ๋‚ด ApolloClient ์ •์˜์ž…๋‹ˆ๋‹ค.

import {ApolloLink, from} from 'apollo-link';
import {application} from '../../constants/application';
import {ApolloClient} from 'apollo-client';
import {InMemoryCache} from 'apollo-cache-inmemory';
import {HttpLink} from 'apollo-link-http';
import {tokenStorage} from '../middleware';
import {debugHelper} from '../helpers/debugHelper';
import {onError} from 'apollo-link-error';
import {apolloCache} from './apolloCache';
import fetch from 'unfetch';

const API = new HttpLink({
    uri: application.API.URL,
    fetch: fetch
});

const cache = new InMemoryCache({
    dataIdFromObject: (object: any) => apolloCache.getID(object)
});

const afterWareLink = onError(({operation, response, graphQLErrors = {}, networkError = {} as any}) => {
    const status: number = networkError && networkError.statusCode ? networkError.statusCode : null;
    debugHelper.error('apolloError', {
        operation,
        response,
        graphQLErrors,
        networkError,
        status,
    });

    // Do your job
    // if (status && HTTPExceptions[status])
    //     redirectService.redirectWithReload(`/error/${status}`);
});

const middleWareLink = new ApolloLink((operation, forward) => {
    const token = tokenStorage.get();
    operation.setContext(context => ({
        ...context,
        headers: {
            ...context.headers,
            token: token ? token.token : '',
        },
    }));

    return forward(operation);
});

export const apolloClient = new ApolloClient({
    link: from([
        middleWareLink,
        afterWareLink,
        API
    ]),
    cache: cache,
});

์œ ์šฉํ•˜๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค.

๋…ธ๋“œ ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉ ์ค‘์ด๊ณ  ์ปจํ…์ŠคํŠธ์—์„œ ์‘๋‹ต ๊ฐœ์ฒด์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ ๋‹ค์Œ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ctx.res.status(401); ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์ „์—.
์ด์ œ apollo-link-errors ๋ฐ apollo-link-retry ๋‘˜ ๋‹ค networkErrors ๊ฐœ์ฒด์—์„œ ์˜ค๋ฅ˜๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ธ์ฆ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ์ž ์ง€์ • ์Šคํ‚ค๋งˆ ์ง€์‹œ๋ฌธ์„ ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์ž…๋‹ˆ๋‹ค.

import { SchemaDirectiveVisitor } from "graphql-tools";
import { defaultFieldResolver } from "graphql";
import { createError } from "apollo-errors";

const AuthError = createError("AuthError", {
  message: "Not authorized"
});

export class IsAuthenticatedDirective extends SchemaDirectiveVisitor {
  visitObject(type) {
    this.ensureFieldsWrapped(type);
  }
  visitFieldDefinition(field, details) {
    this.ensureFieldsWrapped(details.objectType);
  }

  ensureFieldsWrapped(objectType) {
    if (objectType._authFieldsWrapped) return;
    objectType._authFieldsWrapped = true;

    const fields = objectType.getFields();
    Object.keys(fields).forEach(fieldName => {
      const field = fields[fieldName];
      const { resolve = defaultFieldResolver } = field;
      field.resolve = async function(...args) {
        const ctx = args[2];
        const result = await resolve.apply(this, args);
        if (ctx.req.user) {
          return result;
        } else {
          ctx.res.status(401);
          return null;
        }
      };
    });
  }
}

๊ณ ์ณ์กŒ๋‚˜ ์•ˆ๋์–ด? ๋‚˜๋Š” ์—ฌ์ „ํžˆ networkError ๊ฐœ์ฒด์˜ ์ƒํƒœ ์ฝ”๋“œ์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

@artsiompeshko
TypeScript์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const networkErrorLink = onError(({networkError, operation, forward}: ErrorResponse) => {
  if (networkError) {
    switch (networkError['statusCode']) {
      case 401:
      case 422:
        if (window.location.pathname !== '/login') {
          Logger.error('Unauthorized or stale Authorization Session. Reloading page...')
          window.history.go()
        }
        break
    }
  }
  return forward(operation)
})

์ด๊ฒƒ์— ๋Œ€ํ•œ ์†Œ์‹์ด ์žˆ์Šต๋‹ˆ๊นŒ? ๊ฐ™์€ ๋ฌธ์ œ. ๋‚ด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” networkError.statusCode ์—†์Šต๋‹ˆ๋‹ค.

@goldenbearkin ์–ด์ฉŒ๋ฉด ์šฐ๋ฆฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ž˜๋ชป ๋˜์—ˆ์Šต๋‹ˆ๊นŒ?

์˜ค๋ฅ˜์—์„œ ๋„คํŠธ์›Œํฌ ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ํ—ˆ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๋„คํŠธ์›Œํฌ ์ƒํƒœ์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์—†์Šต๋‹ˆ๋‹ค. networkError ๋Š” ์‹ค์ œ๋กœ status ์†์„ฑ์ด ์žˆ๋Š” HttpErrorResponse ์œ ํ˜• ๊ฐœ์ฒด์ด๋ฉฐ ํ•ญ์ƒ 0 ์—ฌ๊ธฐ์„œ ์ฝ˜์†”์˜ ์›๋ž˜ ์ƒํƒœ ์ฝ”๋“œ๋Š” 401 ๋˜๋Š” 403์ž…๋‹ˆ๋‹ค.

@lvlohammadi
graphQLErrors์— ๋Œ€ํ•ด์„œ๋„ ๋™์ผํ•œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ? ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜์—ฌ networkErrors์— ๋Œ€ํ•œ ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ 504๋กœ ํ…Œ์ŠคํŠธํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ธ์ฆ์— ์‹คํŒจํ•˜๋ฉด graphQLError ์ƒํƒœ ์ฝ”๋“œ 401์ด ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ํ˜„์žฌ ์•ก์„ธ์Šคํ•  ๋ฐฉ๋ฒ•์ด ์—†์Šต๋‹ˆ๋‹ค. 'graphQLErrors = {} as any' ์˜ต์…˜์„ ์‹œ๋„ํ–ˆ์ง€๋งŒ ์•„๋ฌด ์†Œ์šฉ์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ ์ด์ƒํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ networkError ์— statusCode ์†์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ธํ„ฐํŽ˜์ด์Šค์—๋Š” ์ด๋Ÿฌํ•œ ์†์„ฑ์ด ์—†์œผ๋ฏ€๋กœ TS๋Š” ์—ฌ๊ธฐ์— ์˜ค๋ฅ˜๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.

const errorLink = onError(({ networkError, graphQLErrors, response }: ErrorResponse) => {
  if (graphQLErrors) {
    graphQLErrors.map(({ message, locations, path }: GraphQLError) => {
      showError(message);
    });
  }

  // @ts-ignore
  console.log('statusCode', networkError.statusCode); // outputs 401

  if (networkError) {
    showError(networkError.message);
  }
});

์„œ๋ฒ„์—์„œ koa ๋ฐ koa-passport ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ํ•ด๋‹น ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

app.use(mount(graphQlPath, passport.authenticate('jwt', { session: false })));

์ด ๋ฌธ์ œ๋Š” ์‹ค์ œ๋กœ ์˜ค๋ž˜๋˜์—ˆ์œผ๋ฏ€๋กœ ๋‹ซ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์—ฌ์ „ํžˆ ์ด์— ๋Œ€ํ•ด ์šฐ๋ คํ•˜๋Š” ๊ฒฝ์šฐ ์–ธ์ œ๋“ ์ง€ ๋‹ค์‹œ ์—ด์–ด์„œ ์ตœ๋Œ€ํ•œ ๋นจ๋ฆฌ ์—ฐ๋ฝ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

@JoviDeCroock ์€ ์ง€๊ธˆ ๊ทธ ๋ฌธ์ œ์— ์ง๋ฉดํ•ด ์žˆ์Šต๋‹ˆ๋‹ค... ๊ทธ๋ž˜์„œ, ํ•ด๊ฒฐ์ฑ…์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? networkError์—์„œ ์˜ค๋ฅ˜ ์ƒํƒœ๋ฅผ ์–ป์œผ๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ?

์ข‹์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๊ทธ๊ฒƒ์ด networkError(graphqlError๊ฐ€ ์—†์Šต๋‹ˆ๊นŒ?)์ด๊ณ  statusCode/status๊ฐ€ ์ •์˜๋˜์ง€ ์•Š์€ ๊ฒƒ์ด ํ™•์‹คํ•ฉ๋‹ˆ๊นŒ?

typescript ์˜ค๋ฅ˜์ž…๋‹ˆ๊นŒ ์•„๋‹ˆ๋ฉด ์‹ค์ œ๋กœ ์ •์˜๋˜์ง€ ์•Š์€ ๊ฒƒ์ž…๋‹ˆ๊นŒ? ์žฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

Btw ๋‚ด ์˜๊ฒฌ์„ ๋ฐ˜๋Œ€ํ•˜๋Š” ๊ฒƒ์€ ์ด ์˜ค๋ฅ˜๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด ๋ฌธ์ œ๋ฅผ ์žฌํ˜„ํ•  ๋ฐฉ๋ฒ•์ด ์—†์—ˆ๊ณ  ์šฐ๋ฆฌ๊ฐ€ ๋„์šธ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค๋“ฌ์–ด์•ผ ํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ์žˆ์ง€๋งŒ ์„œ๋ฒ„์—์„œ ์ƒํƒœ๋ฅผ ๋ณด๋‚ด๋Š” ๋ฐฉ๋ฒ• ๋•Œ๋ฌธ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ graphQL ํŠน์ • ์ฒ˜๋ฆฌ๋ฅผ ๋ฐ”๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ์œผ๋ฉฐ ์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฐ„๋‹จํžˆ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.
res.status(404).send("The resource you are looking for cannot be found")

๊ทธ๋Ÿฌ๋‚˜ ๋‚˜๋Š” TypeError: Failed To Fetch ๋งŒ ์–ป์Šต๋‹ˆ๋‹ค. Apollo-Server-Express์˜ ๊ตฌํ˜„ ์„ธ๋ถ€ ์ •๋ณด ๋•Œ๋ฌธ์ž…๋‹ˆ๊นŒ?

@JoviDeCroock ์‚ฌ์‹ค, 401 ๋˜๋Š” 403์— ๋Œ€ํ•œ ํด๋ผ์ด์–ธํŠธ ์ธก ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ•  ๋•Œ graphql ์ฝ”๋“œ๋กœ ์•ฝ๊ฐ„ ์—‰๋ง์ด๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๋‚ด๊ฐ€ ํ•œ ๊ฒƒ์€ ๋ชจ๋“  ์š”์ฒญ์— โ€‹โ€‹๋Œ€ํ•ด ํ•ญ์ƒ 401์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด OPTION ์š”์ฒญ์—๋„ 401์ด ๋ฐœ์ƒํ–ˆ๋Š”๋ฐ, ๋ฐœ์ƒํ•ด์„œ๋Š” ์•ˆ ๋˜๋Š” ์ผ์ด๋ฉฐ ์•„๋งˆ๋„ ๊ทธ ๋•Œ๋ฌธ์— ์ƒํƒœ๋ฅผ ์ „ํ˜€ ๋ณผ ์ˆ˜ ์—†์—ˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ฝ”๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•œ ํ›„ ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@dimitriy-k ์—…๋ฐ์ดํŠธ์— ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ์ด ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜์—ˆ๋‹ค๋Š” ์†Œ์‹์„ ๋“ค์œผ๋‹ˆ ๊ธฐ์˜๊ฒŒ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

@AlbertoPL์˜ ๊ฒฝ์šฐ ์˜ค๋Š˜ ๋ฐค ๊ท€ํ•˜์˜ ๋ฌธ์ œ๋ฅผ ์กฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ฐ€๋Šฅํ•˜๋ฉด ์ด์— ๋Œ€ํ•ด ๋” ๋งŽ์€ ์˜๊ฒฌ์„ ๋ณด๋‚ด์ฃผ์‹ญ์‹œ์˜ค.

@JoviDeCroock ๋ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ž‘๋™์‹œํ‚ค๋ ค๋Š” ํŠน์ • ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
res.status(426).send()

Chrome์˜ ๋„คํŠธ์›Œํฌ ํƒญ์—์„œ /graphql ์—”๋“œํฌ์ธํŠธ๊ฐ€ 426 ์ƒํƒœ ์ฝ”๋“œ์™€ ์ ์ ˆํ•œ ์‘๋‹ต(์—…๊ทธ๋ ˆ์ด๋“œ ํ•„์š”)์œผ๋กœ ์‘๋‹ตํ•˜๋Š” ๊ฒƒ์„ ๋ด…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ apollo-link-error ์—์„œ onError ๋งํฌ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์—ฌ์ „ํžˆ TypeError: Failed to Fetch ๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ง„์ •ํ•œ ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค๋ฉด ํ›Œ๋ฅญํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค(๋˜๋Š” ๋ฉ”์‹œ์ง€๋กœ๋„ ์ถฉ๋ถ„ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค).

์•Œ๊ฒ ์Šต๋‹ˆ๋‹ค. ์•„์ฃผ ์ข‹์€ ์ •๋ณด์ž…๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์ด๊ฒƒ์„ ์–ด๋–ป๊ฒŒ๋“  ๋””๋ฒ„๊น…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋„ค, ํ™•์‹คํžˆ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋„์›€/ํŒ์„ ์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ Chrome์˜ ๋””๋ฒ„๊ฑฐ์—์„œ ์ฝ”๋“œ๋ฅผ ๋‹จ๊ณ„๋ณ„๋กœ ์‹คํ–‰ํ•˜๋ฉด apollo-link-batch-http ๋ฐ apollo-link-http ๋ชจ๋‘ ๊ฐ€์ ธ์˜ค๊ธฐ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์—ฌ API ํ˜ธ์ถœ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(๋งํฌ ์˜ต์…˜์œผ๋กœ ์ „๋‹ฌ๋˜๊ฑฐ๋‚˜ ์ผ๋ถ€ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ „๋‹ฌ๋จ, ๋‘˜ ๋‹ค ์‹œ๋„ํ•จ).

fetcher(chosenURI, options)
    .then(function (response) {
        operations.forEach(function (operation) { return operation.setContext({ response: response }); });
        return response;
    }).then(parseAndCheckHttpResponse(operations))

then ๋ธ”๋ก์œผ๋กœ ์ด๋™ํ•˜์ง€ ์•Š๊ณ  catch ๋ธ”๋ก์œผ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋•Œ๊นŒ์ง€ ์˜ค๋ฅ˜๋Š” ์ด๋ฏธ "Failed to Fetch"๋กœ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ƒํƒœ ์ฝ”๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http-common/src/index.ts#L118

์ง์žฅ์—์„œ ๊ฝค ๋ฐ”์œ ์ผ์ •์œผ๋กœ ์ด๋ฒˆ ์ฃผ๋ง์— ์žฌํ˜„ํ•ด ๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

ํŽธ์ง‘ํ•˜๋‹ค:

์ฃผ์š” ๋ฌธ์ œ๋Š” ๋‚ด๊ฐ€ ์ด๊ฒƒ์„ ํ…Œ์ŠคํŠธํ•  ๋•Œ๋งˆ๋‹ค apollo-server ํ•˜๊ณ  ์žˆ๊ณ  ์ž˜ ์ง„ํ–‰๋˜์ง€๋งŒ ๋ฐ”์ธ๋”ฉ์ด๋‚˜ ๋ฌด์–ธ๊ฐ€๊ฐ€ ์•ฝ๊ฐ„ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์žฌ์ƒ์‚ฐ ์„œ๋ฒ„ ์—†์ด๋Š” ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

๋„์›€์ด ๋œ๋‹ค๋ฉด ์—ฌ๊ธฐ Apollo ์„œ๋ฒ„๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค(Apollo-server-express๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค). CreateGraphQLSchema๋Š” ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” Apollo Server์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค:

const { ApolloServer, gql } = require("apollo-server-express");

const collectFragments = container => {
  return container
    .$list()
    .map(component => container[component])
    .join("\n");
};

const mergeObjects = container => {
  const types = container.$list().map(k => container[k]);
  return _.merge(...types);
};

function CreateGraphQLSchema(
  schema,
  queries,
  mutations,
  subscriptions,
  resolvers,
  graphqlFormatError,
) {
  const graphQlSchema = gql`
    ${collectFragments(schema.enum)}

    ${collectFragments(schema.type)}

    type Query {
      ${schema.Query}
    }

    ${collectFragments(schema.input)}

    type Mutation {
      ${schema.Mutation}
    }

    type Subscription {
      ${schema.Subscription}
    }

    schema {
      query: Query
      mutation: Mutation
      subscription: Subscription
    }
  `;

  const resolverMap = {};
  resolverMap.Query = mergeObjects(queries);
  resolverMap.Mutation = mergeObjects(mutations);
  resolverMap.Subscription = mergeObjects(subscriptions);
  resolvers.$list().forEach(type => (resolverMap[type] = resolvers[type]));

  return new ApolloServer({
    typeDefs: graphQlSchema,
    resolvers: resolverMap,
    context: ({ req, res }) => {
      return { req, res, user: req.user };
    },
    formatError: graphqlFormatError,
    playground: process.env.NODE_ENV !== "production",
  });
}

@AlbertoPL ๋‚˜๋„์ด ๋ฌธ์ œ์— CORS ๋ฌธ์ œ๋กœ ๋ฐํ˜€์กŒ์Šต๋‹ˆ๋‹ค.

๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ฑฐ๋‚˜ CORS๊ฐ€ ์„œ๋ฒ„ ์ธก์—์„œ ์ž˜๋ชป ๊ตฌ์„ฑ๋˜๋ฉด fetch() ์•ฝ์†์ด TypeError์™€ ํ•จ๊ป˜ ๊ฑฐ๋ถ€๋ฉ๋‹ˆ๋‹ค.

์š”์ฒญ์„ ๊ฑฐ๋ถ€ํ•  ๋•Œ ์ ์ ˆํ•œ ํ—ค๋”๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค. ์ œ ๊ฒฝ์šฐ์—๋Š” API Gateway๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๊ณ  ์‚ฌ์šฉ์ž ์ง€์ • ๊ถŒํ•œ ๋ถ€์—ฌ์ž๊ฐ€ ์š”์ฒญ์„ ๊ฑฐ๋ถ€ํ–ˆ์ง€๋งŒ Access-Control-Allow-Origin ํ—ค๋”๋ฅผ ์ฒจ๋ถ€ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ €๋Š” ์„œ๋ฒ„๋ฆฌ์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐฐํฌ๋ฅผ ๊ด€๋ฆฌํ•˜์ง€๋งŒ ์ด๊ฒƒ์ด ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ์šฐ CloudFormation ํ…œํ”Œ๋ฆฟ์œผ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: https://serverless.com/blog/cors-api-gateway-survival-guide/#cors -with-custom-authorizers.

@chris-feist๋ฅผ ์ž˜ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค!
์‘๋‹ต์— Access-Control-Allow-Origin ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด apollo-client์—์„œ ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

express.js์—์„œ ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

res.set('Access-Control-Allow-Origin', '*').status(401).send()

Access-Control-Allow-Origin์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž ์žฌ์ ์ธ ๋ณด์•ˆ ์œ„ํ—˜์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๊นŒ? https://stackoverflow.com/questions/12001269/what-are-the-security-risk-of-setting-access-control-allow-origin

๊ทธ๋ž˜์„œ ์•„์ง๋„ ์ฐพ๊ณ  ์žˆ๋Š” ๋ˆ„๊ตฐ๊ฐ€๋ฅผ ์œ„ํ•ด...

statusCode ๊ฐ’์€ networkError์˜ ์†์„ฑ์œผ๋กœ ๋ฐ˜ํ™˜๋˜๊ณ  networkError์— ๋Œ€ํ•œ ์—…๋ฐ์ดํŠธ๋œ ์œ ํ˜•์ด ์—ฌ๊ธฐ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. https://github.com/apollographql/apollo-link/pull/530

networkError ๋Š” ์ด์ œ Error | ServerError | ServerParseError ์˜ ๊ณต์šฉ์ฒด ์œ ํ˜•์ด๋ฏ€๋กœ Typescript๋Š” ๊ณต์šฉ์ฒด์˜ ๋ชจ๋“  ์œ ํ˜•์— ๊ณตํ†ต์ ์ธ ์†์„ฑ์—๋งŒ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. statusCode ๋Š” Error ์œ ํ˜•์— ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Typescript๊ฐ€ ๋ถˆํ‰ํ•˜์ง€ ์•Š๊ณ ๋Š” networkError.statusCode ๋ฅผ ํ†ตํ•ด ์•ก์„ธ์Šคํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์œ ํ˜•์„ ๊ตฌ๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด ์œ ํ˜• ๊ฐ€๋“œ ๋˜๋Š” ๋‹ค๋ฅธ ๊ฒƒ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Typescript ํ•ธ๋“œ๋ถ์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ช‡ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์ œ์•ˆ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. https://www.typescriptlang.org/docs/handbook/advanced-types.html#type -guards-and-differentiating-types

๋‚ด ๋ชฉ์ ์„ ์œ„ํ•ด in ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

  onError: ({ networkError }: ErrorResponse) => {
    if (
      networkError &&
      'statusCode' in networkError &&
      networkError.statusCode === 401
    ) {
      // perform logout stuff
    }
  },

@JoviDeCroock ์ด ๋ฌธ์ œ๋ฅผ ๋‹ซ์•„๋„ ์•ˆ์ „ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•˜์‹ญ๋‹ˆ๊นŒ?

@mrkrlli ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๊ดœ์ฐฎ์€ ์†”๋ฃจ์…˜.

์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰