Railsでbefore_filter/before_actionがアクションを中止する仕組みを読んでみる

大昔のRailsでは、before_filterでfalseを返すとそこでchainが終わる、とやっていた気がしますが、今はそういうコード見ないですよね。

Rails 4だとこんなノリでbefore_actionでredirectして はいおしまい、ってやりますよね。

class UsersController < ApplicationController
  before_action :my_authenticate_admin

  def my_authenticate_admin
    unless current_user.admin?
      redirect_to root_path
    end
  end
end

当然、以後のbefore_actionやactionは実行されないことを期待するわけです。
これで全然問題ないのですが、なぜredirect_toするとそこで止まってアクションが実行されないのか、せっかくなので読んでみましょう。

before_actionを探す

まず、before_actionのコードを探します。

actionpack-4.0.0/lib/abstract_controller/callbacks.rb

     [:before, :after, :around].each do |callback|
        class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
          # Append a before, after or around callback. See _insert_callbacks
          # for details on the allowed parameters.
          def #{callback}_action(*names, &blk)
            _insert_callbacks(names, blk) do |name, options|
              set_callback(:process_action, :#{callback}, name, options)
            end
          end

AbstractController::Callbacksにありました。before_action, after_action, around_actionを定義しています。

AbstractController::Callbacksは、includedでこんな処理もしています。

included do
      define_callbacks :process_action, :terminator => "response_body", :skip_after_callbacks_if_terminated => true
    end

あと、controllerのactionを実行するべきprocess_actionが、callbackの中に組み込まれていますね。

callbacksを読む

ActiveSupportのcallbacksを呼び出しているので、そちらを読んでみましょう。

activesupport-4.0.0/lib/active_support/callbacks.rb

      def define_callbacks(*callbacks)
        config = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
        callbacks.each do |callback|
          class_attribute "_#{callback}_callbacks"
          send("_#{callback}_callbacks=", CallbackChain.new(callback, config))
        end
      end

actionpackのほうで呼び出していたdefine_callbacksはここにありました。
CallbackChainはArrayにいくつかメソッドが付いただけのものです。”_process_action_callbacks”という配列ができるようです。

set_callbackはこんな感じです。

      def set_callback(name, *filter_list, &block)
        mapped = nil

        __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
          mapped ||= filters.map do |filter|
            Callback.new(chain, filter, type, options.dup, self)
          end

          options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)

          target.send("_#{name}_callbacks=", chain)
        end
      end

配列に追加してるだけですね。

CallbackChainはArrayですが、1つcompileという重要そうなメソッドがあります。

      def compile
        method =  ["value = nil", "halted = false"]
        callbacks = "value = !halted && (!block_given? || yield)"
        reverse_each do |callback|
          callbacks = callback.apply(callbacks)
        end
        method << callbacks

        method << "value"
        method.join("\n")
      end

お次はapplyです。

      def apply(code)
        case @kind
        when :before
          <<-RUBY_EVAL
            if !halted && #{@compiled_options}
              # This double assignment is to prevent warnings in 1.9.3 as
              # the `result` variable is not always used except if the
              # terminator code refers to it.
              result = result = #{@filter}
              halted = (#{chain.config[:terminator]})
              if halted
                halted_callback_hook(#{@raw_filter.inspect.inspect})
              end
            end
            #{code}
          RUBY_EVAL

いかにもなif文が出てきました。haltedがtrueになると、それ以降callbackを実行しないようになっています。

chain.config[:terminator]を評価した結果がhaltedになっていますが、これは最初に見たAbstractController::Callbacksで”response_body”にセットされていました。

つまり、response_bodyがnilやfalseでないときに、そこでcallback chainが止まるわけですね。

response_bodyを探す

actionpack-4.0.0/lib/abstract_controller/base.rb

  class Base
    attr_internal :response_body

単なるattributeです。

renderやrespond_toを読んでみると、確かにresponse_bodyに書き込んでいます。

actionpack-4.0.0/lib/abstract_controller/metal/redirecting.rb

   def redirect_to(options = {}, response_status = {}) #:doc:
      raise ActionControllerError.new("Cannot redirect to nil!") unless options
      raise AbstractController::DoubleRenderError if response_body

      self.status        = _extract_redirect_to_status(options, response_status)
      self.location      = _compute_redirect_to_location(options)
      self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>"
    end

ということで、before_actionでrenderやredirect_toをすると、response_bodyが空で無くなるため、そこでcallback chainが止まり、以降のbefore_actionやactionが実行されなくなるということが分かりました。

めでたしめでたし。

関連記事

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

baba

ゆとりプログラマー。 高校時代から趣味でプログラミングを初め、そのままコードを書き続けて現在に至る。慶應義塾大学環境情報学部(SFC)卒業。BPS設立初期に在学中から参加している最古参メンバーの一人。得意分野はWeb全般、Ruby on Rails、Androidアプリケーションなど。最近はBlinkと格闘中。軽度の資格マニアで、情報処理技術者試験(高度10区分)などを保有。

babaの書いた記事

週刊Railsウォッチ

インフラ

Rubyスタイルガイドを読む

BigBinary記事より

ActiveSupport探訪シリーズ