Rails 6 APIモードのエラーハンドリング例

こんにちは!kossyです!





さて、今回は、Rails 6 APIモードのエラーハンドリングについて、ブログに残してみたいと思います。



環境
Ruby 2.6.3
Rails 6.0.3
MacOS Mojave



流れとしては、
・捕捉したいエラーをクラス化するerrorsモジュールの定義
・exceptionをjson化してrenderするerror_jsonモジュールの定義
・application_controller.rbで両モジュールをincludeし、class_eval内でrescue_fromを用いて捕捉したいエラークラスを定義


です。

ではコードに注釈を入れたものを晒します。


app/controllers/concerns/errors.rbの作成

module Errors
  # 捕捉したいエラーをハッシュで定義
  HTTPResponseErrors = {
    bad_request: 400,
    unauthorized: 401,
    forbidden: 403,
    not_found: 404,
    internal_server_error: 500,
  }.freeze

  # StandardErrorを継承した新たなクラスを定義
  # アプリケーションレベルの例外であれば、StandardErrorを使うべきという思想に基づいている
  class HTTPResponseError < StandardError
    # エラーコード、エラーメッセージの読み書きができるようにattrsを定義
    attr_accessor :code, :message

    def initialize(args = {})
      # StandardError内のinitializeを呼び出す
      super
      if args.is_a? Hash
        @code    = args[:code] # エラーコード
        @message = args[:message] # エラーメッセージ
      end
      self
    end

    # not_foundをNotFoundのように、_で単語として分割して、それぞれの文字の[0]を大文字にして、連結する。
    def capitalize_with_space(str, delimiter = '_')
      str.split(delimiter).map(&:capitalize).join(' ') if str.present?
    end
  end

  # class_evalメソッドでHTTPResponseErrorをまとめて定義する。
  HTTPResponseErrors.each do |code ,status|
    class_eval <<-RUBY
      class #{code.to_s.camelize} < HTTPResponseError
        def status
          HTTPResponseErrors[:#{code}]
        end

        def code
          @code || "#{code}" # 例: 400, 401等の数値が入る
        end

        def message
          @message || ("#{status} " + capitalize_with_space("#{code}")) # 例: "400 Bad Request" のようにエラーコードに応じた文字列が入る
        end
      end
    RUBY
  end

end

exceptionをrenderするメソッド等を定義するapp/controllers/concerns/error_json.rbを定義

次にerror_jsonモジュールを定義します。

module ErrorJson
  extend ActiveSupport::Concern

  def parse_error_for_json(exception)
    {
      type: 'error',
      status: Rack::Utils.status_code(exception.status), # https://www.rubydoc.info/gems/rack/Rack/Utils#status_code-class_method
      code: exception.code,
      message: exception.message,
    }.to_json
  end

  def render_error(exception)
    render json: parse_error_for_json(exception), status: exception.status
  end
end


Rack::Utils.status_codeメソッドについてはコード内のリンクを参考にしてみてください。

あとは上記2モジュールをapplication_controller.rbに定義して、rescue_fromを定義するだけです。

class ApplicationController < ActionController::API
  # 外部ファイルのConsernのErrorsとErrorActionsをinclude
  include Errors
  include ErrorActions

  # class_evalで400系と500エラーをrescue_fromする
  HTTPResponseErrors.each do |code, status|
    class_eval <<-RUBY
      rescue_from(#{code.to_s.camelize}) {|e| render_error e } # 400 bad_requestの場合 rescue_from(BadRequest) { |e| render_error(e) } のように定義される。
    RUBY
  end
end

rescue_fromでエラーハンドリングを行う場合、withオプションを用いて定義する方法が一般的かも知れませんが、ブロックを渡すやり方もできます。

参考: https://api.rubyonrails.org/classes/ActiveSupport/Rescuable/ClassMethods.html


以上の定義を行うと、controller内で

  raise BadRequest, code: 'invalid_parameter_error' if params[:status_str].is_a?(String)

のように、raiseで例外を起こすと、
application_controller.rbでrescue_fromで予め捕捉したいエラーとして定義されていたBadRequestが捕捉されます。

捕捉された後、render_errorの引数にexceptionが渡され、エラー内容がjsonとして返却されます。

実際の返り値は、

{"type":"error","status":400,"code":"invalid_parameter_error","message":"400 Bad Request"}

のようになります。

これで、controller内でerrors.rbで定義した例外クラス達を使って柔軟にエラーハンドリングを行えるようになります。



参考にさせていただいたサイト
rubyの例外についてまとめてみた - Qiita
【Rails5】rescue_fromによる例外処理:アプリ固有のエラーハンドリングとエラーページ表示 - Qiita