indilog

Ruby/Rails/React/Goなどに関して自分が勉強したことなどを投稿しています

クエリ結果を軸としたGraphQLのエラーハンドリング

この記事は GraphQL Advent Calendar 2020 6日目の記事です。

前回の記事は @fossamagna さんの AppSyncのGraphQL APIを@apollo/clientで呼び出す でした。


この記事では以下の記事で紹介されているGraphQLのエラーハンドリングの手法についての紹介と、それを利用するクライアントサイドのメリットについての考察をしていきます。 sachee.medium.com

アプリケーションで生じる様々なエラーと、GraphQLの一般的なハンドリング

GraphQLはリクエストに対してエラーが発生した場合、一般的にレスポンス中のerrorsというキーの中にそのエラーに関する情報を詰め込んだレスポンスを返すというプラクティスがあります。

"errors": [
    {
      "message": "....",
      "locations": [ ... ],
      "path": [...],
      "extensions": { ... },
    }
  ]

このフォーマットでは、認証エラーやinternal server errorというコンテキストの異なったエラーなどが並列な存在として表現されてしまうので、適切にエラーをハンドリングするためには少し工夫をする必要があります。

実際に世にある様々なGraphQLのライブラリ(graphql-ruby, apollo-server など)ではこのフォーマットを踏襲し、また、エラーを分類可能にさせるためにextensionsにどのようなエラーなのかという情報を入れ込むなどして表現するようにしています。

クエリ対象をリクエストの結果としてモデリングしてみる

errorsに埋め込むことでどこでエラーが出ているのか、何が原因なのか、というところを把握することには事足りそうですが、これを利用するクライアントの立場に立ってみると意外と使いにくいと感じる点がありそうです。 例えばUserを取得するクエリ

{
  user(username: "hoge") {
    id
    name
  }
}

に対してエラーが発生する場合、レスポンスとしては以下などが返ってくるかと思います。

{
   ...,
  "errors": [
    { "path": [ "user" ],
      "locations": [ ... ],
      "extensions": {
        "message": "認証エラー",
        "code": "UNAUTHENTICATED"
      }
    }
  ]
}

この時Userのエラーが存在するかを確かめるためには、リクエストの度にerrors中にそれが存在するのかを配列を舐めて見ていく必要があり、また、エラーの取捨選択や、その詳細度をクエリでコントロールできないため、クライアント側で取り扱う際に不便そうです。

そこでこちらの記事で提案されている、リソースではなく、クエリの結果 をクエリ対象としてモデリングした場合を考えてみます。 sachee.medium.com

この クエリの結果 をクエリする考えを取り入れると、Userをクエリするのではなく、以下のようにリソースとエラーの内容をUnionとしてUserResultを定義することができます。

type User {
  id: ID!
  name: String
}

type IsBlocked {
  message: String
  blockedByUser: User
}

type UnAuthorized {
   reason: String
   role: String
}


union UserResult = User | IsBlocked | UnAuthorized

またこれをクエリする際は以下のようになります。

{
  userResult(username: "hoge") {
    __typename
    ... on User {
      id
      name
    }
    ... on IsBlocked {
      message
      blockedByUser {
        username
      }
    }
    ... on UnAuthorized {
      reason
      role
    }
}

このようにクエリできるようになったことによって、Userに関心があるものはUserResultでまとめて表現することができ、かつクライアント側でその詳細度をコントロールできるようになりました。

特にこの例では、UnAuthorizedな状態の時に、どのroleにおいて認証が通らなかったのかなどを表示する/しないはクライアント側の判断に委ねることができるので、管理画面などで表示したい場合はroleを含ませ、普段ユーザが使う場合は詳細度を絞った形で表現するなど、クライアント側がコントロールできるメリットが生じているのが感じられるかと思います。

また、副次的ではありますが、エラーのパターンを意識してサーバ側はスキーマの設計をする必要があることや、クライアント側はそれをクエリで意識的にクエリできるようになることから、どのようなエラーが発生しうるのか、それをどのようにハンドリングするのか、ということをより意識した設計をすることが可能になるかと思います。

まとめ

  • GraphQLでクエリする対象を、リソースではなく、場合によっては クエリの結果 としてモデリングすることできる
  • クエリの結果としてモデリングすることによって、よりクライアント側が使いやすいエラーの設計が可能になる

プロジェクトの種類や、開発の段階によっては採用の善し悪しが別れそうですが、うまく取り入れられればクライアント側でエラーの内容を柔軟に取り扱うことができ、適切な情報を適切なユーザ/コンテキストで表現することができるようになるのでお試しいただければと思います!