🚨

RailsとGraphQLにおけるエラーレスポンス設計

2024/12/05に公開

はじめに

GraphQLを使用したAPI開発では、エラーハンドリングが非常に重要です。RailsとGraphQLを組み合わせた場合、GraphQL::ExecutionErrorを利用することで、標準的かつ効果的なエラーレスポンスを実装することができます。本記事では、このGraphQL::ExecutionErrorを使ったエラーハンドリングの実装方法と、GraphQL側でのHTTPステータスコードの扱いについて解説します。

GraphQL::ExecutionErrorとは?

GraphQL::ExecutionErrorは、GraphQLクエリの実行中に発生するエラーを表現するための例外クラスです。このクラスを使用することで、エラーメッセージをGraphQLのerrorsフィールドに含める形式でレスポンスに返すことができます。

基本的な使い方

例えば、次のように簡単にエラーを発生させることができます。

# app/graphql/mutations/create_user.rb
module Mutations
  class CreateUser < BaseMutation
    argument :email, String, required: true
    argument :password, String, required: true

    field :user, Types::UserType, null: true
    field :errors, [String], null: false

    def resolve(email:, password:)
      user = User.new(email: email, password: password)
      if user.save
        { user: user, errors: [] }
      else
        raise GraphQL::ExecutionError.new("Invalid input: #{user.errors.full_messages.join(', ')}")
      end
    end
  end
end

この例では、ユーザーの作成に失敗した場合にGraphQL::ExecutionErrorを発生させ、エラーメッセージをクライアントに返しています。これにより、クライアント側では、エラーメッセージをerrorsフィールドから取得できます。

GraphQL::ExecutionErrorの拡張

GraphQL::ExecutionErrorは、デフォルトのエラーメッセージ以外にも、追加のメタデータを持たせることが可能です。例えば、カスタムエラーメッセージにエラーコードやHTTPステータスコードなどの情報を付加することができます。

# app/graphql/errors/custom_execution_error.rb
class CustomExecutionError < GraphQL::ExecutionError
  def initialize(message, extensions = {})
    super(message)
    @extensions = extensions
  end

  def to_h
    super.merge(extensions: @extensions)
  end
end

このクラスを使って、エラーに追加情報を持たせることができます。

# app/graphql/mutations/create_user.rb
module Mutations
  class CreateUser < BaseMutation
    argument :email, String, required: true
    argument :password, String, required: true

    field :user, Types::UserType, null: true
    field :errors, [String], null: false

    def resolve(email:, password:)
      user = User.new(email: email, password: password)
      if user.save
        { user: user, errors: [] }
      else
        raise CustomExecutionError.new(
          "Invalid input: #{user.errors.full_messages.join(', ')}",
          { code: "USER_CREATION_FAILED", status: 422 }
        )
      end
    end
  end
end

この例では、CustomExecutionErrorを使用して、エラーコードやステータスコードを含むカスタムエラーメッセージを返しています。

GraphQLとHTTPステータスコード

GraphQLでは、基本的にすべてのリクエストに対してHTTPステータスコード200を返すことが推奨されています。エラーが発生した場合でも、エラーの詳細はレスポンスのerrorsフィールドに含める形で返します。これは、GraphQLが複数のクエリを同時に実行できる特性を持つため、1つのクエリでエラーが発生しても他のクエリが正常に動作する可能性があるためです。

HTTPステータスコードのカスタマイズ

とはいえ、すべての状況でHTTPステータスコード200を返すことが最適とは限りません。特定のエラー条件に基づいてHTTPステータスコードを変更したい場合、次のように実装します。

# app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  def execute
    result = MyAppSchema.execute(params[:query], variables: params[:variables], context: {}, operation_name: params[:operationName])
    status = result['errors'] ? determine_http_status(result['errors']) : 200
    render json: result, status: status
  end

  private

  def determine_http_status(errors)
    # エラーメッセージ内のステータスコードを参照
    error_status = errors.dig(0, 'extensions', 'status')
    error_status || 200
  end
end

この例では、result['errors']に基づいてHTTPステータスコードを決定しています。エラーメッセージに含まれるカスタムステータスコードがある場合、それを使用してステータスを設定し、特に指定がなければ200を返します。

グローバルエラーハンドリング

Railsアプリケーション全体で統一されたエラーハンドリングを実現するために、GraphQL::ExecutionErrorを使用したグローバルなエラーハンドラーを定義することも可能です。

# app/graphql/my_app_schema.rb
class MyAppSchema < GraphQL::Schema
  rescue_from(ActiveRecord::RecordNotFound) do |err, _obj, _args, _ctx, _field|
    raise GraphQL::ExecutionError.new("Record not found", extensions: { code: "RECORD_NOT_FOUND", status: 404 })
  end

  rescue_from(CustomAuthorizationError) do |err, _obj, _args, _ctx, _field|
    raise GraphQL::ExecutionError.new("Unauthorized access", extensions: { code: "UNAUTHORIZED", status: 401 })
  end
end

このようにして、共通のエラーハンドリングを一箇所にまとめることで、コードの再利用性が高まり、エラーハンドリングが一貫したものになります。

まとめ

GraphQL::ExecutionErrorを活用することで、RailsとGraphQLを組み合わせたAPI開発において、エラーハンドリングをシンプルかつ効果的に実装できます。また、エラーレスポンスにカスタム情報を追加することで、クライアントに対してより詳細で意味のあるエラーメッセージを提供することができます。

さらに、HTTPステータスコードの取り扱いに関しても、デフォルトの200を使うだけでなく、状況に応じたカスタマイズが可能です。グローバルなエラーハンドラーを設定することで、コードの保守性や一貫性を高めることができると思います。

Discussion