around_filterの:ifで指定したブロックが、その前のbefore_filterでチェインが止まってても実行されちゃう

Tsukasa OISHI

以下のようなコードがあるとします。

class OishiController < ApplicationController
  before_filter :check
  before_filter :cc
  around_filter :arou, :if => lambda{|c| @cc.ok?}

  def tsu
    render :text => "ok"
  end

  private

  def check
    # something
  end

  def cc
    @cc = true
    def @cc.ok? ; true end
  end

  def arou
    yield
  end
end

around_filterの:ifに渡すブロック内で、ひとつ前のbefore_filter :ccで定義されたインスタンス変数を使っています。(あまりいい実装ではないですけど)
before_filterのcheckメソッドは、実際にはレアケースの対応を扱っていて、ごくまれにリダイレクトをしたりします。

このコードをRails4で動かしていたときは問題なかったのですが、とある事情でRails3上で動かしたとき、ごくたまに以下の例外が発生していました。

NoMethodError (undefined method `ok?' for nil:NilClass):
  app/controllers/oishi_controller.rb:4:in `_callback_around_19'

調べてみると、before_filterのcheckメソッドでリダイレクトが発生したときにこの例外が出ています。
前のbefore_filterで停止しているのだから、around_filterは呼ばれないはずです。実際、このaround_filterの:if指定をはずしてみると、around_filterのarouメソッドは呼ばれません。
しかし、フィルタチェインが止まっていても、around_filterの:ifブロックは呼ばれているようです。

Railsのコードを追ってみます。filter処理はActiveSupport::Callbacksで実装されているようです。
最終的にaround_filterが呼ばれるときは以下の部分にたどり着きます。

https://github.com/rails/rails/blob/3-2-stable/activesupport/lib/active_support/callbacks.rb#L212

            def #{name}(halted)
              if #{@compiled_options} && !halted
                #{@filter} do
                  yield self
                end
              else
                yield self
              end
            end

@compiled_optionsに:ifのブロック処理が入ります。なので、前のbefore_filterがフィルタチェインを止めていても(halted = true)、:ifブロックは必ず実行されることになります。
この条件判定が逆だったらよかったのかな。