So, I was messing around with a Rails app yesterday, doing the usual respond_to
method thing. Then it hit me that I barely know squat about this method, except that it sends responses in different formats, depending on the request's MIME type. I ended up spending a couple of hours diving into the Rails API docs, guides, and the Rails source code. This post sums up everything I've learned so far.
My biggest takeaway? The number of hoops Rails jumps through to make the final DSL (or API) convenient for the end-users (Rails developers) is mind-boggling. Much respect.
In the previous post on the Rails Router, we learned the concepts of resources and resourceful routing. In its essence, a resource is a key abstraction of information and any information that can be named can be a resource. A single resource can have different representations, also called formats.
For example, consider the Post resource. We could represent a post as an HTML page or as a JSON document.
Now you might be thinking, like, who gets to pick the format in which the response is sent?
The answer is: since HTTP is a client-server protocol, the client (browser) and the server (backend-application) together negotiate the response format. The client tells the server that it's looking for a specific representation, and the server responds with that format.
Let's see how this works in practice, in the context of a Rails application.
When the browser sends an HTTP request to your Rails application, it provides a list of formats that it can understand and interpret. For this, the browser uses the Accept
HTTP header.
For example, to indicate that it will accept only HTML responses, the browser can set the Accept
header to text/html
(check out the common MIME types).
// accept HTML
Accept: text/html
// OR, accept images
Accept: image/*
// OR, accept anything
Accept: */*
The browser can also pass multiple formats.
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*
Upon receiving the request from the browser, the server (Rails app) then chooses a format from the provided list or declines the request.
Specifically, this process of negotiating the response format is called content negotiation. Content negotiation determines how a specific representation is selected when the client requests a resource.
- When a client wants to obtain a resource, the client requests it via a URL, passing the format via the
Accept
header or the URL extension (see below). - The server uses this URL to choose one of the variants available–each variant is called a representation–and returns a specific representation to the client.
URL Format Recognition in Rails
Every route you create in your Rails application automatically recognizes the format for URLs that end with a dot (.
) and the format parameter, which is optional. That's why, when you print routes using bin/rails routes
command, you see (.:format)
at the end of each URI pattern.
Prefix Verb URI Pattern Controller#Action
posts GET /posts(.:format) posts#index
POST /posts(.:format) posts#create
For example, if the client requests the URL yourapp.com/posts/first-post.json
, the server treats it in the same way as if the browser had set the Accept
header to application/json
.
$ curl -v http://yourapp.com/posts/first-post.json
GET /posts/first-post
Host: yourapp.com
Accept: */*
Since you didn't specify the Accept
header explicitly, curl used * / * , indicating that it accepts any MIME type.
So far, we've only seen how the client sends the format types it needs. We don't know how the your Rails application (server) handles the request to send the response in the correct format. Let's explore that now.
How the respond_to Method Works
The Rails controller accepts the incoming HTTP requests and returns the format requested by the client. The respond_to
method provides an elegant DSL inside the controller actions, allowing you to return different results based on the format requested by the client.
For example, consider the ReportsController#show
action which returns the report either as an HTML page or a PDF or JSON document, depending on the requested format.
class ReportsController < ApplicationController
def show
@report = Report.find(params[:id])
respond_to do |format|
format.html
format.pdf
format.json { render json: @report.to_json }
end
end
end
The first time I saw the above code in Rails, my procedural brain, coming from C# and .NET, couldn't comprehend what was happening. My first impression was we were first returning an HTML response, then a PDF, and finally, the JSON.
Why send three responses for one request? 🤔
I couldn't be more wrong. This is what the above code is saying, instead:
So, in the respond_to
block, we are only configuring what should happen when the client needs a particular MIME type. When the request comes, Rails automatically determines the desired response format from the HTTP Accept header submitted by the client, and sends the corresponding response format.
What happens when the client requests a format that's not included in the respond_to
block? Rails simply returns a 406 Not Acceptable status, to indicate that it can't handle the request.
The respond_to
method also allows you to specify a common block for different formats by using any
:
def show
@product = Product.first
respond_to do |format|
format.html
format.any(:xml, :json) { ... }
end
end
With that understanding, let's try to briefly understand how the respond_to
works.
First things first. The respond_to
is just a method that accepts a block that is used to define responses to different mime-types.
respond_to do |format|
format.html
format.xml { render xml: @people }
end
In this example, the argument passed to the block, i.e. format
is an instance of the ActionController::MimeResponds::Collector
class. This class acts as a container for responses available from the current controller for requests for different mime-types (formats). However, if you open the Collector
class, you don't see any of those methods in there.
What gives? 🤔
The Collector
class uses its method_missing
to dynamically register the methods (this is where it's defined).
- When you call the
html
method without any arguments or block, it instructs Rails to handle routine HTML requests using regular views, i.e. templates, and layouts. - When we call
xml
, we are telling it to respond to requests with the.xml
extension by serializing to XML format.
There are two ways to call respond_to
: Either pass a list of accepted MIME types or pass a block (as shown above), but you can't pass them both. The first version is recommended when you’re not doing anything special to render your resources but still want to support multiple MIME-types.
def index
@people = Person.all
respond_to :html, :js
end
# OR
def index
@people = Person.all
respond_to do |format|
format.html
format.js
format.xml { render xml: @people }
end
end
Declare Custom MIME Types
If you need to use a MIME type which isn’t supported by default, you can register your own handlers in config/initializers/mime_types.rb
as follows.
Mime::Type.register "image/jpeg", :jpg
For example, the turbo-rails
gem defines a custom MIME type called text/vnd.turbo-stream.html
to handle Turbo Stream responses.
# lib/turbo/engine.rb
initializer "turbo.mimetype" do
Mime::Type.register "text/vnd.turbo-stream.html", :turbo_stream
end
Then you can use it in your controllers as follows:
def destroy
@message = Message.find(params[:id])
@message.destroy
respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.remove(@message) }
format.html { redirect_to messages_url }
end
end
Custom Variants for a Format
Sometimes, you may want to render different templates for phones, tablets, and desktop browsers. For this, Rails supports format variants. Each format can have different variants, i.e. a specialization of the request format, like :tablet
, :phone
, or :desktop
.
You can set the variant in a before_action
:
request.variant = :tablet if /iPad/.match?(request.user_agent)
Respond to variants in the action just like you respond to formats:
respond_to do |format|
format.html do |variant|
variant.tablet # renders app/views/projects/show.html+tablet.erb
variant.phone { extra_setup; render ... }
variant.none { special_setup } # executed only if there is no variant set
end
end
Provide separate templates for each format and variant:
app/views/projects/show.html.erb
app/views/projects/show.html+tablet.erb
app/views/projects/show.html+phone.erb
For more details, check out the respond_to
documentation.
That's a wrap. I hope you found this article helpful and you learned something new.
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.