Middleware in Rails: What It Is, How It Works, and Examples

In this post, We'll learn about Rails middleware: what it is, why we need it, how it works, and why it's so important. If you have a fuzzy understanding of middleware, this post will make it concrete. I'll also show you how to create and test custom middleware for your Rails app.

The Middleware Pipeline
The Middleware Pipeline

P.S. I suggest that you read my previous post "The Definitive Guide to Rack for Rails Developers" before reading this one. That should give you a good context for this post. If you're already familiar with Rack, keep reading.

As we learned in the previous post, the Rack protocol provides a simple interface for web applications and web servers to talk to each other. This communication happens through the middleware pipeline.

In this post, we're going to do a deep dive into middleware; especially how and where they fit into your Rails applications. We'll learn what middleware is, why we need them, how they work, and why they're so important. I'll also show you how to create custom middleware for your Rails application and how to test it.

Sounds good? Let's get started.

I've also planned a series of posts explaining each and every middleware Rails uses (the ones you see by running bin/rails middleware command). If you'd like to receive them in your email, please subscribe to my blog. As always, a big hug and thank you if you're already a subscriber.

What You'll Learn:

What is Middleware?

A middleware is nothing but a class with a call method that recieves the incoming HTTP request (env) from the web server, processes it before passing it to the Rails application, processes the response received from the application, and returns the response back to the web server.

When we say an application is Rack-compliant, it means the following:

  1. It has a call method that accepts a single argument env, containing all the data about the request, and
  2. It returns an array containing the status, headers, and response.

Because the Rack interface is so simple, you can use any code that implements this interface in a Rack application, allowing you to build small, focused, and reusable applications that work together to provide different functionalities. These mini-components are known as Middleware.

It's best to envision middleware as a series of "layers" HTTP requests must pass through before they hit your application. Each layer can examine the request, modify it, and even reject it entirely.

Here's a simple middleware that prints some text before and after passing the request to the application (or the next middleware in the pipeline).

class CustomMiddleware
  def initialize(app)
    @app = app
  end
  
  def call(env)
    puts 'before'
    result = @app.call(env)
    puts 'after'
    result
  end
end

Two important things to note here:

  1. The middleware receives the application (or the next middleware) in the constructor.
  2. After receiving the response from the application, it has to return the response to the web server, or the next middleware in the pipeline.

This diagram will help you understand better.

Middleware
Middleware

Why Use Middleware?

Middleware is very useful for writing logic that is not specific to your web application, such as authenticating the request, logging, or error handling. It focuses on doing one thing and doing it well.

Middleware sits between the user and the application code. When an HTTP request comes in, the middleware can intercept, examine, and modify it. Similarly, it can examine and modify the HTTP response before forwarding it to the user.

Middleware provides a convenient mechanism for inspecting and filtering HTTP requests entering your application. For example, Rails includes the Rack::MethodOverride middleware that inspects and overrides the HTTP verb of the incoming request.

Using middleware also simplifies your application code, and it can only focus on the logic related to the application.

Authentication is another good use case for middleware. If the user is not authenticated, the middleware will redirect the user to your application's login screen. However, if the user is authenticated, the middleware will allow the request to proceed further into the application.

Additional middleware can be written to perform a variety of tasks besides authentication. For example, a logging middleware might log all incoming requests to your application.

There are several middleware (is 'middlewares' a word?) included in the Rails framework. You can see a list of all middleware by running the bin/rails middleware command. The Rails guides also provide a brief description of what each middleware does.

In the next section, we'll learn how you can create your own middleware.

Create Custom Middleware

Let's say you want to add a new middleware for your application that does a simple thing: verify a token sent in the header by the client code. If this token matches our secret token, allow the request to proceed. Otherwise, immediately return an error without passing the request to the application.

How do you do that?

Before writing any new code, let's add some unit tests first.

  1. In the first test, we'll pass the secret token in the headers, and expect that the application will allow access and return a normal response.
  2. In the second test, we won't pass any token, on which the application should return an error.
require "test_helper"

class TokenAccessTest < ActionDispatch::IntegrationTest
  test 'allows access for valid tokens' do
    get middleware_path, headers: { 'token' => 'my-secret-token' }
    assert_equal 'success', response.body
  end

  test 'returns error for invalid or missing tokens' do
    get middleware_path
    assert_equal 'invalid or missing token', response.body
  end
end

Let's take care of the plumbing first, by adding a route and a controller#action to handle the incoming request. I'll add a /middleware route, which is accessible as middleware_path that will be dispatched to the MiddlewareController#show method.

# config/routes.rb
get 'middleware', to: 'middleware#show', as: 'middleware'

# app/controllers/middleware_controller.rb
class MiddlewareController < ApplicationController
  def show
    render plain: 'success'
  end
end

If I run the tests now, the second test fails as we're not verifying the token at all. Let's fix that by introducing a custom middleware, which is just a Ruby class with the interface we saw in the introduction. As mentioned above, it will have a call method that takes the incoming request and returns an array containing the HTTP status, headers, and the response body.

Where do you put middleware in your Rails app?

Middleware can't be in app because they can't be reloaded. They should be in lib. - Rafael Franca

Okay, let's create a new folder called middleware in the lib folder and add VerifyToken middleware in it. Its constructor receives the app object which is your application, also a middleware.

# lib/middleware/verify_token.rb

module Middleware
  class VerifyToken
    def initialize(app)
      @app = app
    end
  
    def call(env)
      request = ActionDispatch::Request.new(env)
      if request.headers['token'] != 'my-secret-token'
        return [200, {}, ['invalid or missing token']]
      end
  
      @app.call(env)
    end
  end
end

As you can see, in the call method, we'll first verify the token and return an error if it doesn't match our secret token. If it matches, we send the normal response.

Makes sense?

If anything doesn't make sense at this point, please leave a comment below and I'll try to clarify it the best I can.

We're not done yet, we need to tell Rails to insert this middleware in the middleware stack. Let's do this using the config.middleware object.

Middleware API in Rails

Rails provides a simple configuration interface config.middleware for adding, removing, and modifying the middleware stack.

The object returned by config.middleware method is an instance of Rails::Configuration::MiddlewareStackProxy class. As the name suggests, it's a proxy for the Rails middleware stack that lets you configure the middleware for your application.

You can either config.middleware in the application.rb file or one of the environment-specific configuration files under the environments/<env>.rb file.

To add a new middleware, use the config.middleware.use method, passing the name of the Ruby class that acts as middleware. Rails will insert this middleware at the end of the existing middleware stack, meaning it will get executed in the end, just before your application is called.

# config/application.rb

require_relative "../lib/middleware/verify_token"'

module Blog
  class Application < Rails::Application
    config.middleware.use Middleware::VerifyToken
  end
end

Run the tests again, and they should pass this time! We've successfully used middleware to verify an incoming request.

Instead of putting your middleware in the end, if you wish to insert it before or after another middleware, you can do so using the insert_before or insert_after method.

config.middleware.insert_before ActionDispatch::Callbacks, VerifyToken

This will insert the VerifyToken callback before the ActionDispatch::Callbacks middleware runs.

Before vs. After

Middleware can perform tasks before or after passing the request deeper into the application. For example, the following middleware performs some task both before and after the request is handled by the application:

module Middleware
  class DoSomething
    def initialize(app)
      @app = app
    end
  
    def call(env)
      # perform something before passing the request to the application
      process_request(env)
      
      response = @app.call(env)
      
      # perform something after receiving the response from the application. 
      # It should also return the response.
      process_response(response)
    end
  end
end

No matter where a middleware performs its action, it still has to return a valid Rack-compatible response to the next middleware in the pipeline, or the web server.

Modify Existing Middleware

In addition to inserting your own middleware, Rails also lets you modify its existing middleware stack, by allowing you to replace or even delete a middleware.

Replace Middleware

You can replace existing middleware in the middleware stack using config.middleware.swap method. For example,

config.middleware.swap ActionDispatch::RequestId, Custom::RequestId

will replace the ActionDispatch::RequestId module with the Custom::RequestId module.

Delete Middleware

You can delete existing middleware using the config.middleware.delete method.

config.middleware.delete ActionDispatch::Flash

Now the flash middleware won't run when Rails executes the middleware pipeline. You can also verify that it's removed by running the bin/rails middleware command.

Passing Parameters to Middleware

So far, we've run our middleware code without providing any additional parameters. Rails also makes it easy to pass extra parameters. For example, we can pass a default token for the VerifyToken middleware as follows:

# config/application.rb

# Set custom middleware
config.middleware.use Middleware::VerifyToken, default_token: 'another-token'

This parameter will be available in the constructor of the middleware. You can set it to an instance variable and access it anywhere else in the class.

module Middleware
  class VerifyToken
    def initialize(app, default_token: nil)
      @app = app
      @default_token = default_token
    end
  
    def call(env)
      # remaining code
    end
  end
end

You can pass more than one parameters, if needed.

How to Test Middleware Classes

If you're adding a custom middleware with significant code or logic, it's a good idea to add tests for it. That said, you can simply test the side-effect of the middleware without directly testing the middleware class, as we've seen earlier, and that should be totally fine for most cases.

However, what if you want to actually test the VerifyToken class? How to initialize its dependency app , which is the whole Rails application itself?

Don't worry, you can pass the world's simplest Rack app instead ;)

require "test_helper"
require "middleware/verify_token.rb"

class VerifyTokenTest < ActiveSupport::TestCase
  setup do
    @app = ->(env) { [200, { 'Content-Type' => 'text/plain' }, [ 'success' ]] }
    @middleware = Middleware::VerifyToken.new(@app)
    @env = Rack::MockRequest.env_for("/middleware")
  end

  test 'deleting the associated quotes' do
    response = @middleware.call(@env)
    assert_equal [200, {}, ["invalid or missing token"]], response
  end
end

Note that we're using Rack::MockRequest to build the env hash.

Now that you have all the necessary dependencies, you can test your custom middleware to your heart's content.

That’s a wrap. I should probably stop here. If you're still reading this (and I know you are), you should have a much better understanding of the concept of middleware, instead of it just being a fuzzy concept at the back of your mind.

Here're some great resources for you to learn more about middleware and solidify your understanding.  

Additional Resources


I hope you liked this article 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 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.

Subscribe to Akshay's Blog

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe