rails ECブログリレー

Rails APIのエラーレスポンスを定義する

rails ECブログリレー

記事の目的と背景

EC事業部SCXチームのryuchan00と言います。

先日、RailsでゼロからレスポンスとしてJSONを返却するAPIを実装する機会がありました。その時点では、僕は今までエラーハンドリングの仕組みまでは最初から実装したことがありませんでした。例外処理を実装する方法として、各アクションにrescue節を書く方法があります。この方法は、アプリケーションの規模が小さいときはコントローラの数が少ないため例外処理の実装は苦にはなりづらいです。しかし、コントローラの数が多くなってくると各コントローラのアクションに似たような例外処理を書く必要があります。また、同じ例外クラスを補足している箇所に変更を施すときは、その全てに対して同じ修正が必要になり管理が難しくなります。この課題をすでに解決している事例が社内にはありました。その解決方法は、例外処理を1つのモジュールで管理して、ApplicationControllerにincludeしてApplicationControllerを継承する方法です。この記事では、その実装について説明し、実装の過程で学んだことを紹介します。なお、Railsの基礎を少しだけ知っている必要があるため、この記事の想定する読者はRailsアプリケーションを実装する人です。

エラーレスポンスの仕様

JSON APIのエラーレスポンスの設計と実装は、RFC78071を参考にできます。ただし、複数のエラー情報をクライアントに返したい場合はRFC7807には方法が明記されていません。一つのリクエストに対して複数の理由でエラーになってしまった場合は1回のレスポンスでクライアント側にエラー詳細を伝えることができれば何回もリクエストを投げずに済むと思い、「title」「detail」をラップするオブジェクトを設けるデータ構造にしました。ここでは「errors」とします。下記がエラーが1つだった場合のレスポンスの一つの例です。複数になった場合は、errors配下のオブジェクトが増えることになります。

{
  "errors": [
    {"title": "見つかりませんでした。", "detail": "Userが見つかりませんでした。"},
  ],
  "status": 404
}

Railsアプリケーションのコード

では、実際のRailsアプリケーションのソースコードの一部を紹介します。Userリソースを返すUsersControllerでは、ActiveRecord::RecordNotFoundというエラーが発生する可能性があります。このエラーは、UsersController以外でも同じように発生する可能性があるため、このエラーが発生したときの処理をErrorRenderableモジュールに共通化しました。このモジュールをApplicationControllerにincludeすることで、すべてのコントローラでのRecordNotFoundが発生した場合、ErrorRenderableモジュールで例外処理が行われるようになります。以下のコードはRuby 2.6.5、Rails 6.0.3で動作確認しています。

app/controllers/concerns/error_renderable.rb

module ErrorRenderable
  extend ActiveSupport::Concern

  included do
    rescue_from ActiveRecord::RecordNotFound do |e|
      render json: { errors: { title: 'レコードが見つかりません', detail: 'IDと一致するレコードが見つかりません' } }, status: :not_found }
    end
  end
end

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include ErrorRenderable
end

app/controllers/users_controller.rb

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  end
end

この実装を通して学んだこと

この実装を通しての学びはActiveSupport::Rescuable::ClassMethods#rescue_from 2です。恥ずかしい話ですが、今までrescueでのみ例外処理をしてきたのでActiveSupport::Rescuable::ClassMethods#rescue_fromを使ったことがありませんでした。このメソッドを用いれば特定の例外をコントローラ全体で扱えるようになり便利でした。

結論

僕がRailsガイドを探した限り、JSON APIでの複数のコントローラのエラー処理を共通化についての記載を見つけることができませんでした。しかし、社内のソースコードを探すと実装に参考にできそうなものがありました。僕は今回実装に使用したメソッドの1つ1つはどこかで見たことはありました。しかしこれらを組み合わせて例外処理をするという発想がありませんでした。今回の実装をするにあたり僕はModule#includedActiveSupport::Rescuable::ClassMethods#rescue_fromなどの仕様を一つ一つ確認する機会になりました。もし記事を読んでいる人の中でJSON APIの実装で迷っている方がいれば是非実装案の一つとして見てもらえればと思います。