How respond_to method works in Rails

How respond_to Method Works in Rails

The respond_to method allows the controller to select the appropriate response format based on the request's Accept header or the request URL. You can also use it to handle variants for different screens. This post covers the basics of this method: what it is, how it works and why it's important.

6 min read

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.

💡
The Accept request HTTP header indicates which content types (also known as MIME types) the client is able to understand. The server uses content negotiation to select one of the proposals and informs the client of the choice with the Content-Type response header. Source

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.

  1. 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).
  2. 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.
💡
Although I'm using the term 'browser' for the client, it doesn't have to be. You can use curl or any other program to make HTTP requests and pass the Accept HTTP header. In curl, you can set the request header with the -H option.

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:

💡
If the browser wants to see the report HTML in the browser, render the reports/show.html.erb template as usual. If they want the report as a PDF, render the corresponding PDF template. However, if they want JSON, return the report data as a JSON document.

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 mime-type-specific methods like html, xml are dynamically generated on the fly, using Ruby's method_missing method.

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.