RailsとGraphQLにおけるエラーレスポンス設計
はじめに
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