Tuesday, February 8, 2011

DoubleRenderError and :before_filter

A ActionController::DoubleRenderError occurred in 
(some action):

Render and/or redirect were called multiple times 
in this action. Please note that you may only call 
render OR redirect, and at most once per action. 
Also note that neither redirect nor render 
terminate execution of the action, so if you want 
to exit an action after redirecting, you need to 
do something like "redirect_to(...) and return".

When you get a DoubleRenderError, there is a good chance that something is happening as part of an action that you didn't anticipate, because you expected all action to stop when you did a render or redirect_to. So, bad things may be happening, like ignoring authentication, executing the action anyway, and then displaying to the user that either that he was unauthorized or displaying a page that is the result of a DoubleRenderError.

One cause of this is particularly insidious. :before_filter keeps track of whether a render or redirect_to was called and, if so, will stop the action. If you move a method previously called by the :before_filter to the action method itself, you may forget to return from the action if that method calls render or redirect_to. If you are lucky there will be another render or redirect_to and this will result in a DoubleRenderError, so you'll catch it and perhaps be alerted that something went wrong. However, an action getting executed unintentionally is not a good thing.

To avoid this, be sure that when you call a method that might render or redirect for that method to return a boolean that you can use in the calling method to return from the action.

The following authenticate method is fine when called by a :before_filter:

class MyController < ApplicationController

  :before_filter authenticate

  def authenticate
    render :file => "public/401.html", :status => :unauthorized unless authorized?
  end

  def some_action
    # do something important and possibly render or redirect_to
  end

end
However, after moving this to the action method, you need to do a return:
class MyController < ApplicationController

  def authenticate
    auth = authorized?
    render :file => "public/401.html", :status => :unauthorized unless auth
    return auth
  end

  def some_action
    return unless authenticate
    # do something important and possibly render or redirect_to
  end

  def some_other_action
    # this action is not authenticated or is authenticated differently,
    # but still makes sense to include in this controller
  end

end

No comments: