LoginSignup
25
22

More than 5 years have passed since last update.

カスタム例外を小さな exceptions_app でハンドリングする

Last updated at Posted at 2016-10-21

目的

Rails アプリケーションで例外をハンドリングするには ApplicationController で rescue_from を使う、Rack ミドルウェアを使う、exceptions_app を使う、Rambulance といった Gem をインストールするなど色々な方法があります。今回はなるべくシンプルな方法を取りたかったので exceptions_app としてコントローラのアクションを利用する方法を採用してみました。 なお、そういう意味で「小さな exceptions_app」と呼称しました。

具体例

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  class NotAuthorizedError < StandardError; end

  ### 略 ###
end

ApplicationController::NotAuthorizedError という例外を独自に用意し、コントローラのアクション内でそれを raise した場合に 403 Forbidden を返すようにしたいです。とりあえず JSON リクエストのみを考慮します。

手順

1. exceptions_app 用のコントローラを作成し、アクションを実装する

app/controllers/errors_controller.rb
class ErrorsController < ActionController::Base # not ApplicationController
  def show
    exception = request.env['action_dispatch.exception']
    status = request.path[%r{(?<=\A/)\d{3}\z}] # request.path[1..-1] でも OK

    render(json: { error: exception.message, status: status }, status: status)
  end
end

JSON レスポンスのボディとなる { error: exception.message, status: status } の部分は任意です。

2. 1. で作成したコントローラのアクションを exceptions_app に設定する

config/initializers/exceptions_app.rb
Rails.configuration.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }

ActionController::Metal.action は Rack エンドポイントとなる Proc オブジェクトを返します。

3. Rails.application.config.action_dispatch.rescue_responses を編集する

config/application.rb に以下を追記します。

config/application.rb
config.action_dispatch.rescue_responses.update(
  'ApplicationController::NotAuthorizedError' => :forbidden
)

例外と HTTP ステータスの組み合わせは ActionDispatch::ExceptionWrapper.rescue_responses で確認できます。このコードを実行すると、一番下に ApplicationController::NotAuthorizedError が追加されているのが分かります。

ActionDispatch::ExceptionWrapper.rescue_responses
{
  "ActionController::RoutingError" => :not_found,
  "AbstractController::ActionNotFound" => :not_found,
  "ActionController::MethodNotAllowed" => :method_not_allowed,
  "ActionController::UnknownHttpMethod" => :method_not_allowed,
  "ActionController::NotImplemented" => :not_implemented,
  "ActionController::UnknownFormat" => :not_acceptable,
  "ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
  "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
  "ActionDispatch::ParamsParser::ParseError" => :bad_request,
  "ActionController::BadRequest" => :bad_request,
  "ActionController::ParameterMissing" => :bad_request,
  "Rack::Utils::ParameterTypeError" => :bad_request,
  "Rack::Utils::InvalidParameterError" => :bad_request,
  "ActiveRecord::RecordNotFound" => :not_found,
  "ActiveRecord::StaleObjectError" => :conflict,
  "ActiveRecord::RecordInvalid" => :unprocessable_entity,
  "ActiveRecord::RecordNotSaved" => :unprocessable_entity,
  "ApplicationController::NotAuthorizedError" => :forbidden
}

なお、config/application.rb ではなく config/initializers 配下の Ruby ファイルで同様のコードを実行しても追加されないので注意です。

以上で完了です。

動作確認

development 環境で exceptions_app の動作を確認するときは、config.consider_all_requests_local の値を一時的に false に変更します。

config/environments/development.rb
# Show full error reports.
config.consider_all_requests_local = false # デフォルトは true

では、適当なコントローラのアクションで例外を発生させて、動作を確認してみます。ステータスコードが 403 となるか、そして JSON レスポンスの内容が想定通りかをチェックします。

class UsersController < ApplicationController
  def index
    raise(NotAuthorizedError, 'Rails の許可ぁ?認められないわぁ')

    ### 略 ###
  end
end
Processing by UsersController#index as JSON
### 略 ###
Processing by ErrorsController#show as JSON
### 略 ###
Completed 403 Forbidden in 1ms (Views: 0.2ms | ActiveRecord: 0.0ms)

スクリーンショット 2016-10-21 15.47.27.png

きたわぁ 👏

1T7ZA.jpg

参考

25
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
22