Working with HTTP Requests in Rails

Working with HTTP Requests in Rails

Every web application needs to process incoming HTTP requests. In this post, we’ll take a closer look at how Rails handles requests, how you can access the request object in the controller, and some of the most useful methods it provides to gather meaningful data from the request.

8 min read
This is the third post in my deep-dive series on Rails Controllers. Check out the first two posts here: Introduction to Rails Controllers and Understanding Rails Parameters. More posts coming soon...

HTTP is a request-response based protocol. The browser (acting as the client) sends an HTTP request to the server (your Rails application). The server then parses the request, processes it, and generates an HTTP response, which is sent back to the browser. Each request carries a bunch of useful information that your application frequently relies on to handle the request properly.

Rails offers a simple and straightforward interface to work with the current HTTP request hitting your application. It also exposes a bunch of super useful methods to access details like request data, including input parameters, URLs, paths, and the request body that comes with each request.

In this post, we’ll explore some of the most commonly used methods for inspecting incoming HTTP requests. First, let’s take a look at how you can access the request object inside a controller class.

Accessing the Request

To obtain an instance of the current HTTP request in your controller class, use the request method, which returns an instance of the ActionDispatch::Request class.

class RequestsController < ApplicationController
  def show
    puts request  # #<ActionDispatch::Request:0x0000000128d38c08>
  end
end

Now you might be wondering where this request method (is it even a method?) is defined in the Rails controller hierarchy. So let's inspect the Rails codebase, specifically the ActionController::Metal class, which is the great-granddaddy of your application controllers.

module ActionController
  class Metal
    # The ActionDispatch::Request instance for the current request.
    attr_internal :request
  end
end

The attr_internal method is provided by Rails, not part of standard Ruby. It declares an attribute reader and writer backed by an internally-named instance variable, @_request. This is particularly useful when you want to avoid naming conflicts with public methods or when you want to clearly distinguish internal state variables.

It’s worth noting that the request object is only accessible within controllers — this is intentional as a controller is the correct level of abstraction to handle incoming requests.

Just wanted to make that clear, as I often see questions like, “How can I access the request object in my models or service objects?” The short answer is: you don’t. Instead, extract only the necessary data from the request in the controller and pass that data to your models or service objects, instead of trying to access to whole request in there.

The ActionDispatch::Request instance provides a bunch of methods to examine the incoming HTTP request. Let's take a look at some of the important and commonly used methods below.

Retrieving the Request Path

The path method returns the request's path information. If the user visits your application at https://basecamp.com/projects/rails, the path method will return projects/rails.

class ProjectsController < ApplicationController
  def rails
    url_path = request.path # /projects/rails
  end
end

Now, if you're like me and your first instinct is to take a look at the Rails API docs every time you come across a new method, you'd be surprised to find that the list of methods listed under the ActionDispatch::Request class doesn't have a path method. So where's that coming from?

As always, let's open the Rails codebase and take a peek at the source code for the ActionDispatch::Request class. Here's the important part:

module ActionDispatch
  class Request
    include Rack::Request::Helpers
  end
end

Let's follow the trail and open the Rack::Request::Helpers module located in the Rack gem (run the bundle open rack command). Here's the source code.

module Rack
  class Request
    module Helpers
      def path
        script_name + path_info
      end
    end
  end
end

So that's where the path method is coming from. The ActionDispatch::Request class includes the Rack::Request::Helpers module, which provides the path method.

However, the path method only returns the URL path component, and doesn't include any query parameters. If you want the full path, including any query params if present, you'll have to use the fullpath method on the request.

# URL: https://example.com/projects/rails?page=22

class ProjectsController < ApplicationController
  def rails
    path = request.path           # /projects/rails
    fullpath = request.fullpath   # /projects/rails?page=22
  end
end

In practice though, if you need to work with the query params, you'd typically use the params object instead of reading them from the URL. For example, given the same URL endpoint above, you'd access the page query param as follows:

puts params[:page]  # 22

To learn more about Rails parameters, check out my previous post in the series on Rails controllers:

Understanding Rails Parameters
Rails parameters let you access data sent by the browser, both via the URL and forms. In this article, we’ll cover the basics of parameters, including what they are, how they work, and why they’re important. We’ll also learn how you can pass parameters in different formats such as arrays and hashes.

Retrieving the Request URL

To access the complete URL including the domain for the incoming request, you can use the url method.

class ProjectsController < ApplicationController
  def rails
    url = request.url     # http://www.example.com/projects/rails?page=22
    path = request.path   # /projects/rails
  end
end

Retrieving the Request Host

If you just want the domain name, you can retrieve the host of the incoming request via the host method.

puts request.host # www.example.com

Retrieving the Request Method

The method method returns the HTTP verb for the request.

request.method  # "GET"

The nice thing about this method is that it will return the original value of the request method, even if it was overridden by any middleware.

Working with Headers

The headers method on ActionDispatch::Request provides access to the request's HTTP headers.

request.headers["Content-Type"] # "text/plain"

Here's the implementation of this method:

# actionpack/lib/action_dispatch/http/request.rb

def headers
  @headers ||= Http::Headers.new(self)
end

The Http::Headers provides a hash-like storage, giving you all the headers associated with this request. You can get and set a value of a particular header just like you get and set values for a hash.

Important: If you just call the headers method in a controller, it will give you the response headers, i.e. headers sent with the response.

class ProjectsController < ApplicationController
  def rails
    headers == response.headers  # true
  end
end

To access the request's headers, remember to use request.headers as follows:

class ProjectsController < ApplicationController
  def rails
    http_method = request.headers["REQUEST_METHOD"]
  end
end

There is one gotcha when accessing headers like this. If a particular header doesn't exist, Rails will return nil, which may not what you want. Sometimes, you want to know if the header is indeed not present, and use a default value if it's missing. For this, use the fetch method on the Headers class.

It accomplishes two things: When the header is not present, it will raise a KeyError. Alternatively, you can pass an optional second argument that will be returned if the headers is not present on the request.

class ProjectsController < ApplicationController
  def rails
    request.headers.fetch("X_REQUEST_METHOD")  # KeyError: X_REQUEST_METHOD
    request.headers.fetch("X_REQUEST_METHOD", "GET")  # "GET"
  end
end

Finally, you can use the key? or include? methods to check if a particular header value is present or not, just like a regular Hash.

Request IP Address

💡
In general, treat IP addresses as untrusted, user-controlled input and use them for information-purposes only.

You can use the ip method to retrieve the IP address of the user (or client) that make the HTTP request to your Rails application.

ip_address = request.ip  # "127.0.0.1"

In addition, Rails also provides a remote_ip method on the request object. In development (or often in production), you will see the same value for both.

remote_ip_address = request.remote_ip  # "127.0.0.1"

So, what's the difference between the ip and remote_ip methods?

Sometimes, the client doesn't directly send the request to you, but via intermediate proxies, each of which forwards it to the next proxy before the request reaches your application. In these cases, the server would only see the final proxy's IP address, which is not that useful, as it's often a load balancer.

To solve this, each proxy is supposed to set the X-Forwarded-For header. This header identifies the originating IP address of a client connecting to a server via a proxy server.

X-Forwarded-For: <client>, <proxy>, …, <proxyN>

When a request goes through multiple proxies, the IP addresses of each successive proxy are listed. The rightmost IP address is the IP address of the most recent proxy and the leftmost IP address is the address of the originating client.

Rails provides a RemoteIp middleware which calculates the IP address of the remote client making the request, and exposes it via remote_ip method on the request.

Don't use this middleware if you're not using at least one proxy, otherwise it exposes you to IP spoofing attack (since any client can claim to have any IP address by setting the above HTTP header).

Accessing Request Body

If you are building an API or any sort of web services, you often want to work with the raw, unchanged request body. For a long time, I used to add this helper method in my Rails controllers to access the incoming request body.

def request_body
  @request_body ||= (
    request.body.rewind
    request.body.read
  )
end

However, Rails provides an awesome raw_post method which does it for you. In your Rails controller, you can access this method as follows:

request.raw_post
💡
Here's an interesting fact: The raw_post method was introduced by Toby Lütke, the founder of Shopify.

Unique Request ID

In Rails, each request gets its own unique identifier, useful for end-to-end tracing of a request, logging, and even for debugging. This is set by the ActionDispatch::RequestId middleware and you can access it using the request_id method on the request.

class ProjectsController < ApplicationController
  def rails
    id = request.request_id  # "1659c4b4-1557-43dc-8883-f03968888f32"
  end
end

You can also use request.uuid which is an alias of the above method.

Summary

Here's a quick glance at most of the methods available on the request object.

require "test_helper"

class ProjectsControllerTest < ActionDispatch::IntegrationTest
  test "methods on the request object" do
    post projects_publish_path(tag: "programming"), params: { title: "Ruby on Rails", description: "A web framework for Ruby" }, headers: { "X-Project-Id" => "58432" }

    assert_equal "title=Ruby+on+Rails&description=A+web+framework+for+Ruby", request.raw_post
    assert_equal 56, request.content_length
    assert_equal "/projects/publish", request.path
    assert_equal "/projects/publish?tag=programming", request.fullpath
    assert_equal "58432", request.headers["X-Project-Id"]
    assert_equal "127.0.0.1", request.ip
    assert request.local?
    assert_equal "application/x-www-form-urlencoded", request.media_type
    assert_equal "POST", request.method
    assert_equal :post, request.method_symbol
    assert_equal {"tag"=>"programming"}, request.query_parameters
    assert request.request_id
    assert_equal {"title"=>"Ruby on Rails", "description"=>"A web framework for Ruby", "tag"=>"programming", "controller"=>"projects", "action"=>"publish"}, request.parameters
  end
end

Hope that helps.


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. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.