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

# 目的

Rails アプリケーションで例外をハンドリングするには ApplicationController で [rescue_from](http://api.rubyonrails.org/v5.0/classes/ActiveSupport/Rescuable/ClassMethods.html) を使う、Rack ミドルウェアを使う、exceptions_app を使う、[Rambulance](https://github.com/yuki24/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](http://api.rubyonrails.org/classes/ActionController/Metal.html#method-c-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` が追加されているのが分かります。

```rb
ActionDispatch::ExceptionWrapper.rescue_responses
```
```rb
{
  "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 レスポンスの内容が想定通りかをチェックします。

```rb
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](https://qiita-image-store.s3.amazonaws.com/0/6697/8f6dbe51-9255-4c46-21f0-d83ddae5fe9b.png)

きたわぁ 👏

![1T7ZA.jpg](https://qiita-image-store.s3.amazonaws.com/0/6697/219c84a4-976a-5c43-e679-6ae7f63627d5.jpeg)

# 参考

* [Railsアプリの例外ハンドリングとエラーページの表示についてまとめてみた](http://qiita.com/upinetree/items/273ae574f1c021d24c37)
    * ほとんどこの記事を参考にしました。多謝 🙇 
* [Ruby on Rails Guides > Configuring Rails Applications > Configuring Rails Components > Configuring Action Dispatch](http://guides.rubyonrails.org/configuring.html#configuring-action-dispatch)