Respond With An Explanation

Using respond_to and respond_with is, as Rails tends to be, Convention Over Configuration™. This is a wonderful thing, but proficiency requires an understanding of these conventions which, in the case of respond_with, may be less than intuitive. What follows is an attempt to shed some light on this new(ish) feature of Rails.

We recently ran into an issue with Devise on a client project. As is often the case, we wanted a new user to be redirected to a custom page after registering, and followed the excellent instructions here. We defined a new after_sign_up_path_for method in the appropriate controller, yet the route specified in after_sign_up_path_for wasn’t being rendered.

We did a bit of digging into the Devise source, and found that what was ultimately being called in its RegistrationsController#create was this:

respond_with resource, :location => after_sign_up_path_for(resource)

This is why redefining after_sign_up_path_for in your controller allows you to change the post-signup location. Cool, right? Only … this wasn’t working. To understand why it wasn’t working, we needed to dig into how respond_with works.

As always, Ryan Bates explains it best. Paraphrasing the relevant information: the most important thing to know is that respond_with is specific to an HTTP verb. If it’s a GET, it first looks for a view for that particular action and MIME type. If it can’t find that, it essentially calls #to_<br /> on the object you give it. However, if it’s a POST, it will validate the object (and send it back to the appropriate form, if necessary), and then basically call redirect_to @object.

However, that’s not quite the whole story: respond_with ALWAYS looks for a corresponding template for an action, regardless of verb and MIME type, and regardless of whether a :location is passed to it or not (this behavior isn’t really documented anywhere, at least that we could find).

So can you guess what we found in our codebase, after discovering this behavior? That’s right, a big, fat HTML template for the fubars#create action.

To demonstrate, if we have a resources :fubar line in our routes file, and a corresponding controller:

class FubarsController < ApplicationController
  respond_to :html, :xml

  def create
    @fubar = Fubar.create(params[:fubar])
    respond_with @fubar
  end
end

then after creating a new Fubar we’ll be sent to its show page as we would expect.

If we modify the create action to be

def create
  @fubar = Fubar.create(params[:fubar])
  respond_with @fubar, :location => root_path
end

then we’ll be sent to our application’s root_path after we create a new Fubar, and all is right with the world.

However, in both cases, if we happen to have an HTML template file for the create action, say in app/views/fubars/create.html.erb, then that template will be rendered instead. Always.

Regarding the :location option, the Edge Rails API says it all:

Two additional options are relevant specifically to respond_with  -
 1. :location - overwrites the default redirect location used after
    a successful html post request.
 2. :action - overwrites the default render action used after an
    unsuccessful html post request

Now, notice that both :location and :action are only relevant for an HTML request. This means that using the :location option and having an HTML template are mutually exclusive. Okay, not exactly mutually exclusive: you can have both, but the location you specify in respond_with will never be used.

In short, this:

respond_with @fubar, :location => root_path

is equivalent to this:

respond_to do |format|
  format.html { redirect_to root_path }
  format.xml { render :xml => @fubar }
end

unless you have an HTML template for fubars#create, in which case it will be the same as this:

respond_to do |format|
  format.html
  format.xml { render :xml => @fubar }
end

Category: Development
Tags: Tutorial