読者です 読者をやめる 読者になる 読者になる

Railsで例外処理403/404を実装してmodule切り分け【実践Rails学習ノート#5】

はじめに

追記@2016/06/22

Railsでの例外処理についてこの後一度真剣に考えてみたので、先に貼っておきます。

totutotu.hatenablog.com

本編

Rails学習ノート続きです。

今回は403 Forbiddenの例外処理を作っていきたいと思います。 

おおまかに以下のような流れで書いていきます。

  • 例外を補足する
  • Viewをつくる
  • 例外処理をつくる

403 Forbidden

  403 Forbiddenのステータスコードは、要求されたリソースはサーバー上にあるのだけどアクセス権やIPアドレス制限などの問題でアクセス拒否されたことを表します。

例外を補足する

まずは例外を補足するための処理をcontrollerに書いていきます。

新しくActionController::ActionControllerErrorクラスを継承したForbiddenクラスと、IpAddressRejectedクラスを定義しておきます。

そのあとにForbiddenクラスとIpAddressRejectedクラスが発生した時に実行するメソッドを指定して、そのメソッドrescue403を定義します。

# app/controllers/application_controller.rb
...
  class Forbidden < ActionController::ActionControllerError; end
  class IpAddressRejected < ActionController::ActionControllerError; end

  rescue_from Exception, with: :rescue500
  rescue_from Forbidden, with: :rescue403
  rescue_from IpAddressRejected, with: :rescue403
...
  def rescue403(e)
    @exception = e
    render 'errors/forbidden', status: 403
  end

Viewを作成

Viewはapp/views/errors/forbidden.slimに以下のように作りました。

# app/views/errors/forbidden.slim
#error
  h1 403 Forbidden
  - case @exception
  - when ApplicationController::IpAddressRejected
    = "あなたのIPアドレス(#{request.ip})からは利用できません。"
  - else
    = "指定されたページを閲覧する権限がありません。"

Rubyの埋め込みの書き方ちょっと迷ってしまった。あと明示的なendを避けなければいけないことにも注意。

例外を起こす

そして、top#indexでraiseメソッドでIpAddressRejectedクラスの例外をわざと起こすようにしてみます。

class Admin::TopController < ApplicationController

  def index
    raise IpAddressRejected
  end

end

errorページが表示されました!IPアドレスが表示され、クラスはApplicationController::IpAddressRejectedになっていることも確認できます。

404 Not Found

リソースのないURLにアクセスされた場合RailsActionController::RoutingErrorを発生しますが、これだとルーティング処理の段階で発生する例外を保続できません。そこで少し工夫を加えます。

まず、config/routes.rbの最後に以下を加えます。

  root 'errors#routing_error'
  get '*anything' => 'errors#routing_error'

これで、全てのルーティングに一致しなかった場合最後にerrors#routing_errorメソッドが呼ばれます。

$ bin/rails g controller errors

ActionController::RoutingErrorのときはrescue403のメソッドを呼ぶように設定します。statusでレスポンスのコードを渡しているのもRails流儀。

# application_controller.rb
  rescue_from ActionController::RoutingError, with: :rescue404
...
  def rescue404(e)
    @exception = e
    render 'errors/not_found', status: 404
  end

そしたら作成したErrorsControllerを以下のように書き換えてerrors#routing_errorメソッドを定義します。

class ErrorsController < ApplicationController
  def routing_error
    raise ActionController::RoutingError,
      "No route matched #{request.path.inspect}"
  end
end

そしたらViewを作成します。

#error
  h1 404 not found
  p 指定されたページは見つかりません。
  p.url = request.url 

これで404も表示されるようになったかと思います。

controllerのmodule切り出し

app/controllers/application_controller.rbに例外処理が増えてごちゃごちゃしてきたのでこいつをまとめていきます。

app/controllers/concern/error_handling.rbというファイルを新たに作成して以下のようにします。application_controller.rbで定義したメソッドなどをこちらに移します。

module ErrorHandlers
  extend ActiveSupport::Concern
  
  included do
    rescue_from Exception, with: :rescue500
    rescue_from ApplicationController::Forbidden, with: :rescue403
    rescue_from ApplicationController::FIpAddressRejected, with: :rescue403
    rescue_from ActionController::RoutingError, with: :rescue404
    rescue_from ActiveRecord::RecordNotFound, with: :rescue404
  end

  private 
  def rescue500(e)
    @exception = e
    render 'errors/internal_server_error', status: 500
  end

  def rescue403(e)
    @exception = e
    render 'errors/forbidden', status: 403
  end

  def rescue404(e)
    @exception = e
    render 'errors/not_found', status: 404
  end
end

app/controllers/concernsRails 4.0で導入されたディレクトリです。さっきみたいにcontrollerが冗長になりそうになったときには、こちらにmoduleとして定義してincludeすることによってfatなcontrollerになってしまうことが防げるってわけみたいです。

ActiveSupport::Concernという仕組みで、2行目の

extend ActiveSupport::Concern

でmoduleがこのクラスを継承していることがわかります。これによりincludedメソッドが使えるようになります。

pry> ls ActiveSupport::Concern
constants: MultipleIncludedBlocks
ActiveSupport::Concern.methods: extended
ActiveSupport::Concern#methods: append_features  class_methods  included

includedメソッドに囲われたメソッドは、このクラスをincludeしたクラスのメソッドとして扱われるようにできます。

またForbiddenクラスなどがApplicationController::Forbiddenなどに変わったことにも注意。前はApplicationControllerクラスの中での定義だったので名前空間の中だったけど今回は外に出てしまっているので指定が必要。

とういことでapplication_controllerからErrorHandlersモジュールをincludeします。

# app/controllers/application_controller.rb
    include ErrorHandlers if Rails.env.production?

またproductionのときだけincludeすることで、普段の開発では普通のRailsの例外処理のページを表示することができます。これめっちゃ便利やんけ。

application_controllerはすぐfatになりがちなので、早い段階から切り取ってmodule化していくことを意識したほうがよさげです。

大事なこと

今回学んだ大事なことをまとめておきます。

  • application_controllerは肥大化しやすい
  • のでActiveSupport::Concernsを使ってcontrollerをmodule化する