目的
Rails アプリケーションで例外をハンドリングするには ApplicationController で rescue_from を使う、Rack ミドルウェアを使う、exceptions_app を使う、Rambulance といった Gem をインストールするなど色々な方法があります。今回はなるべくシンプルな方法を取りたかったので exceptions_app
としてコントローラのアクションを利用する方法を採用してみました。 なお、そういう意味で「小さな exceptions_app」と呼称しました。
具体例
class ApplicationController < ActionController::Base
class NotAuthorizedError < StandardError; end
### 略 ###
end
ApplicationController::NotAuthorizedError
という例外を独自に用意し、コントローラのアクション内でそれを raise
した場合に 403 Forbidden を返すようにしたいです。とりあえず JSON リクエストのみを考慮します。
手順
1. exceptions_app 用のコントローラを作成し、アクションを実装する
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 に設定する
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.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
に変更します。
# 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)
きたわぁ 👏