Rack Middlewares in Ruby on Rails
In this post, I will describe what Rack is and how to write middlewares in Ruby on Rails. I will use common production examples.
Imagine that we want to take request samples in our App and send the metrics to a Service to analyze (request processing time, http_status, etc.). At the same time, we want to add an ID to every single request for debugging purposes. Before jumping into how to solve it, we need to understand a few concepts.
What is a Middleware?
The term middleware is used in many different forms and has different meanings depending on the context.
A middleware is a layer that lives between the application and the server. It acts as the middleman between them. The communication flow between a client, a server and its middleware, is the following:
The middleware receives a request from a client (1) and sends it to the application (2). The application returns a response (3). The middleware grabs that response and sends it to the client (4). See figure 1 above.
Usages
As you might think from the definition, middlewares have a lot of power!. We can change the request before passing it to the application and after it is processed before sending it to the client!.
There are many different usages for middlewares:
- Routing: We can redirect a request depending on its content.
- Performance: We can modify the request before passing it to the application to improve performance (e.g. compressing responses).
- Logging: We can log the request stats before sending it to the client (e.g. log the response status or time to process the request).
- Security: We can verify that the request was made by a client that we trust.
- Profiling: (e.g. the gem rack-mini-profiler)
- And many more!
Rack
What is Rack? Do I use it? If you’re working on Rails, you likely use it!
First of all, Rack is a gem. It’s is included in Rails by default (via ActionPack). It’s also part of other popular Web Ruby frameworks like Sinatra or Hanami.
It allows you to write middlewares for Ruby apps easily! Rack creates an interface that wraps HTTP requests and responses. A Rack application is a Ruby object (not a class) that responds to the call
method. The application is called when the method call
is invoked.
Rack provides a minimal, modular, and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call. Rack source code.
A Rack application needs to be Rack compliant. It means that it needs to respect the Rack spec. The application needs to respond to call
that receives one argument: the environment
and returns an Array of exactly three values: status
, headers
, and body
.
The enviroment
is a hash that contains CGI-like headers such as http_method
, query_string
etc., and Rack specific variables. We can modify the environment before calling the App, such as adding a header with extra information or return early if a particular condition happens. You can see here the list of values an environment can have.
Pro tip: To see the list of middleware your Rails app has, run the command: bin/rails middleware
.
Writing Middleware Applications with Rack
From the code above, you probably got a good idea of how to write Rack middleware. It’s straightforward!. First, we need to add it to your App’s middleware stack. To do that, we need to use config.middleware
in the application.rb
file to tell in which class our middleware lives and the order it’s in the stack (More details).
Let’s see two examples.
Example 1: Log Request and Response metrics
We want to print, in the console, metrics of the requests and responses, such as the controller used, time to process the request, etc.
The constructor of our Rack middleware will receive the application and store it in a variable.
We will store the current time in a variable and tell the App to execute (by calling app.call
). We will calculate the time and print the metrics in the console.
A similar middleware Rails::Rack::Logger is implemented in Rails.
Example 2: Add a unique identifier to each request
We want to add a unique identifier to each request, tell the application which ID the request has, and add it to the response.
To do that, we’ll generate a random ID and add it to the request’ headers before passing the request to the App. We have access to the headers via the environment.
Great! We have added a request-ID identifier to every request!
You might have seen this in Rails applications before. It is because ActionDisptach has a middleware called RequestId that implements the same functionality.
I encourage you to take a look at the Rails source code to check the middlewares included. You can see the complete list on this file. You can also check the examples used here on this Github repository.
Conclusion
We learned the basics of Middlewares, what Rack is, and how easy it’s to write them in Rails.
Middlewares are very powerful. There are thousands of use cases for them. We covered two examples, which are examples of existing middleware that are part of the Rails framework.
Thanks for reading!