おおいしつかさ


旅行とバイクとドライブと料理と宇宙が好き。
Ubie Discoveryのプログラマ。
Share:  このエントリーをはてなブックマークに追加

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

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

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ブロックは必ず実行されることになります。
この条件判定が逆だったらよかったのかな。