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 -
- :location - overwrites the default redirect location used after a successful html post request.
- :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