When you generate a scaffold in Rails, you’ll see the usual respond_to
blocks:
def destroy
@task.destroy
respond_to do |format|
format.html { redirect_to tasks_url, notice: 'Task was successfully destroyed.' }
format.json { head :no_content }
end
end
But some of your actions, like index
, don’t have them!
# GET /tasks
# GET /tasks.json
def index
@tasks = Task.all
end
This is bad. Why? If you hit /tasks.txt
, and txt
isn’t supported by your app, you’ll get the wrong error:
ActionView::MissingTemplate (Missing template tasks/index, application/index with {:locale=>[:en], :formats=>[:text], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}
This isn’t quite right. You should be telling the client that they’re requesting a format you don’t support, not that you can’t find the right file.
If this was a UnknownFormat
error, you could return a better response code. Instead, these errors will get mixed together with other, unrelated errors, and it’ll be really hard to handle them.
You could add a respond_to
block to your index
action:
# GET /tasks
# GET /tasks.json
def index
@tasks = Task.all
respond_to do |format|
format.html
format.json
end
end
Then, you’d get the exception and error code you’d expect:
Started GET "/tasks.txt" for 127.0.0.1 at 2014-11-03 22:05:12 -0800
Processing by TasksController#index as TEXT
Completed 406 Not Acceptable in 21ms
ActionController::UnknownFormat (ActionController::UnknownFormat):
app/controllers/tasks_controller.rb:8:in `index'
Much better. But littering all your controllers with respond_to
is crazy. It feels un-Rails-ish. It violates DRY. And it distracts you from the work your controller is actually doing.
You still want to handle bad formats correctly. So what do you do?
A respond_to
shortcut
If you’re not doing anything special to render your objects, you can take a shortcut. If you write:
def index
@tasks = Task.all
respond_to :html, :json
end
it works the same way as writing the full respond_to
block in index
. It’s a short way to tell Rails about all the formats your action knows about. And if different actions support different formats, this is a good way to handle those differences without much code.
Handle formats at the controller level
Usually, though, each action in your controller will work with the same formats. If index
responds to json
, so will new
, and create
, and everything else. So it’d be nice if you could have a respond_to
that would affect the entire controller:
class TasksController < ApplicationController
before_action :set_task, only: [:show, :edit, :update, :destroy]
respond_to :html, :json
# GET /tasks
# GET /tasks.json
def index
@tasks = Task.all
respond_with(@tasks)
end
And this actually works:
Started GET "/tasks.txt" for 127.0.0.1 at 2014-11-03 22:17:37 -0800
Processing by TasksController#index as TEXT
Completed 406 Not Acceptable in 7ms
ActionController::UnknownFormat (ActionController::UnknownFormat):
app/controllers/tasks_controller.rb:8:in `index'
Exactly the kind of error we were hoping to get! And you didn’t have to mess with each action to do it.
Sometimes you’ll want to do different things depending on the state of a model. For instance, for create
, you’d either redirect or re-render the form, depending on whether or not the model is valid.
Rails can handle this. But you still have to tell it which object you want it to check, with respond_with
. So instead of:
def create
@task = Task.new(task_params)
respond_to do |format|
if @task.save
format.html { redirect_to @task, notice: 'Task was successfully created.' }
format.json { render :show, status: :created, location: @task }
else
format.html { render :new }
format.json { render json: @task.errors, status: :unprocessable_entity }
end
end
end
you can write:
def create
@task = Task.new(task_params)
flash[:notice] = "Task was successfully created." if @task.save
respond_with(@task)
end
This way, you separate your code from the formats you respond to. You can tell Rails once which formats you want to handle. You don’t have to repeat them in every action.
The responders gem
In Rails 4.2, there’s a catch: respond_with
is no longer included. But you can get it back if you install the responders
gem. And the responders
gem brings some other nice features with it.
You can set flash messages in respond_with
by including responders :flash
at the top of your controller:
class TasksController < ApplicationController
responders :flash
Conveniently, you can set defaults for these flash messages in your locale files.
Also, if you have the responders
gem in your Gemfile
and you generate a Rails scaffold, the generator will create controllers using respond_with
instead of respond_to
:
class TasksController < ApplicationController
before_action :set_task, only: [:show, :edit, :update, :destroy]
respond_to :html, :json
def index
@tasks = Task.all
respond_with(@tasks)
end
def show
respond_with(@task)
end
# ...
This is a lot cleaner than the scaffolds Rails comes with.
And finally, if you want to only respond with a format for specific controller actions, you can call respond_to
multiple times:
class TasksController < ApplicationController
respond_to :html
respond_to :js, only: :create
end
Thanks to Jeroen Weeink in the comments for that last tip!
respond_with
or respond_to
?
If you want to return different information for different formats, you have a few options. The controller-level respond_to
combined with respond_with
is a great way to get short controllers. But it tends to help the most when all of your controller actions respond to the same format, and act in the way Rails expects them to.
Sometimes, though, you want to be able to have a few actions that act differently. The one-liner respond_to
is great for handling that situation.
If you need more control, use the full respond_to
with a block, and you can handle each format however you want.
With any of these, requests for formats you don’t support will get the right error. And both your app and its clients will be a lot less confused.