GraphQL error formatting

신연우·2021년 4월 3일
0

Express로 개발할 때는 편했는데

REST API를 Express를 사용해 개발할 때는 response의 형식이 거의 정형화되어 있어서 반환할 때도 그 형식을 그대로 사용하면 됐었다.

// 평소 사용하던 response 반환 형태
{
  message: "Response Message",
  status: 200 || 404 || ...
}

하지만 GraphQL은 달랐다. 일단 평소 반환 형태랑 다르다는 것은 공식 문서를 통해 알고 있었지만 가장 큰 문제는 error의 기본 반환 형태였다.

// 예시
{
  "message": "user query failed",
  "locations": [
    {
      "line": 5,
      "column": 3
    }
  ],
  "path": ["user"],
  "extensions": {
    "code": "INTERNAL_SERVER_ERROR",
    "exception": {
      "stacktrace": [
        "Error: user query failed",
        "    at user (/sandbox/index.js:31:13)",
        "    at field.resolve (/sandbox/node_modules/graphql-extensions/dist/index.js:133:26)",
        "    at resolveFieldValueOrError (/sandbox/node_modules/graphql/execution/execute.js:467:18)",
        "    at resolveField (/sandbox/node_modules/graphql/execution/execute.js:434:16)",
        "    at executeFields (/sandbox/node_modules/graphql/execution/execute.js:275:18)",
        "    at executeOperation (/sandbox/node_modules/graphql/execution/execute.js:219:122)",
        "    at executeImpl (/sandbox/node_modules/graphql/execution/execute.js:104:14)",
        "    at Object.execute (/sandbox/node_modules/graphql/execution/execute.js:64:35)",
        "    at /sandbox/node_modules/apollo-server-core/dist/requestPipeline.js:240:46",
        "    at Generator.next (<anonymous>)"
      ]
    }
  }
}

일단 에러가 한 번 발생하면 stack trace가 반환되는 문제가 있다. 길이가 길어진다는 점도 있지만, 불필요한 정보까지 전달된다. 물론 불필요한 정보는 상황에 따라 stack trace만이 아닐 수도 있다.

심지어, 여기서 message, location, path는 여러 개의 에러가 발생하면 errors 배열 안에 들어오게 된다.

error : {
  errors: [
    {
      message: "Argument Validation Error",
      // 생략
    },
    {
      message: "Argument Validation Error,
      // 생략
    }
  ]
}

흔히, class-validator를 활용한 arguments validation에서 이러한 현상이 자주 발생한다.

물론 생기는 에러를 그대로 반환해서 프론트가 처리하라고 할 수도 있지만, 그건 너무 비효율적이다. 이렇게 긴 정보를 보낼 필요도 없고, 꼭 필요한 것만 담아서 전달해주는 것이 보안적으로도, 개발 편의성쪽으로도 옳은 것이라 생각했다.

그리고 무엇보다 class-validator로 인해 validation된 에러는 Apollo-Server에 등록된 에러 중에서 UserInputErrorValidationError가 아닌 InternalServerError로 나오게 된다(실제로 extensions.code 값을 보면 "INTERNAL_SERVER_ERROR"로 출력된다. 깜짝 놀랐다).

프론트가 평소에 받던 형태로 반환해보자

이 부분은 자료를 찾아보니 Apollo-Server의 인스턴스를 생성할 때, formatError 프로퍼티에 함수를 넘겨주면 error를 핸들링할 수 있다.

new ApolloServer({
    schema,
    context,
    formatError  <-- 이 부분에 err => { ... }를 전달하면 된다.
  })
// custom formatError callback
export const formatError = err => {
  let { message, extensions } = err;
  console.log(message)

  // class-validator and graphql validation exception handling
  if (err instanceof ValidationError || message.startsWith("Argument")) {
    message = "Invalid Parameteres";
    extensions = { status: 400 };
  }

  return {
    message: extensions && extensions.status ? message : "Internal Server Error",
    status: extensions && extensions.status ? extensions.status : 500
  };
};

평소 프론트엔드가 개발할 때 사용하던 형식을 그대로 전달할 수 있도록 처리하고 싶어 평소 사용하던 형태를 최대한 구현하도록 노력했다.

우선 error 객체 내에서 message 항목과 extensinos 항목을 꺼내온다. 이때 class-validator에 의해 던져진 에러를 확인하고 이 에러의 경우 특별히 반환을 해 준다.

여기서 주의할 점은 class-validator에 의해 던져지는 에러는 required parameter는 전부 전달되었고, 그 에러들 중 지정된 형식에 맞지 않는 경우에 던져지는 에러다.

즉, required parameter가 전달되지 않은 경우는 ValidationError가 먼저 throw되기 때문에 이 또한 같이 핸들링해줘야 한다.

{
  "error": {
    "errors": [
      {
        "message": "Invalid Parameteres",
        "status": 400
      },
      {
        "message": "Invalid Parameteres",
        "status": 400
      },
      {
        "message": "Invalid Parameteres",
        "status": 400
      }
    ]
  }
}

하지만, 위와 같이 여러 개의 에러가 발생한 경우 배열 내에 에러가 담겨서 전달된다. 이는 graphql의 특징으로 여러 개의 요청을 전달했을 때 각자의 결과에 따라 에러가 발생할 때마다 formatError에 등록한 함수가 실행되는 것이다.

실제로 위 경우 필요한 4개의 인자 중 1개만 전달했을 때의 상황이며, 실제 콘솔 상에서도 formatError에 등록한 함수가 3번 호출된 것을 알 수 있었다.

이 부분은 formatError에서도 해결할 수 없을 것 같아 다른 방법을 찾아보기로 했다.

참고: stack trace만 없애고 싶다면

기본적으로 GraphQL이 제공하는 에러에서 stack trace 항목만 없애고 싶다면

new ApolloServer({
    schema,
    context,
    debug: false
  })

위와 같이 debug options을 false로 설정하면 된다.

회고

사실 이 부분 처리를 위해 2일이나 이것만 생각하고 있었다. 사실 여기서 끝이 아니라 이런 형태로 error를 반환하기로 했으면 일부 기능들 중에서는 평소 반환하는 것처럼 response도 formatting할 필요가 있었다. 다음 글에서는 response formatting 과정에 대해 알아보도록 하자.

profile
남들과 함께하기 위해서는 혼자 나아갈 수 있는 힘이 있어야 한다.

0개의 댓글