Alberto Grespan

Rack middleware

January 31, 2015

tl;dr we are creating two Rack middlewares, one that adds a custom header to all responses and one that adds a new route with a custom response.

As explained in a previous post about Rack basics, Rack is found on the most popular Ruby web frameworks, as it’s an adaptable interface for developing web applications. In this post we are going to create two simple middlewares. The first one will add a custom header to all responses, the second one will add a custom route that responds to /ping and to glue it all together we are using Rack::Builder.

Things to know about Rack middleware

There are few things that we need to know about Rack middleware in order to fully understand how it works are:

Now that we know this, let’s start writing the middleware.

Adding a custom header

For this first middleware we are going to add a custom header to all responses. We’ll be using the following header: X-Custom-Header: customheader.v1. We could also use something like Content-Type: application/json; charset=utf-8 if we wanted to.

note: our middleware will live in a lib directory.

# lib/custom_header.rb
module Rack
  class CustomHeader
    def initialize(app)
      @app = app
    end

    def call(env)
      status, headers, body = @app.call(env)

      headers['X-Custom-Header'] = "customheader.v1"

      [status, headers, body]
    end
  end
end

As explained in the section above, we are creating the call() method and passing it the env argument. Inside this method we picking up the status, headers and body from the @app.call(env). Then appending our custom header to the existing response headers and finally return the array of status code, headers, and body.

We have our first middleware ready, let’s go with the second one.

Adding a new route

Inside this middleware we are adding a new route called /ping. This route will respond with a pong text. How will this middleware work? It will check the request path, if it’s /ping it will respond if it doesn’t, the call will pass to the rest of the stack.

# lib/ping.rb
module Rack
  class Ping
    def initialize(app)
      @app = app
    end

    def call(env)
      req = Rack::Request.new(env).path
      if req == "/ping"
        [200, {}, ["pong"]]
      else
        @app.call(env)
      end
    end
  end
end

In the same way as with the CustomHeader middleware, there will be a call() method with the env argument. Inside this method we’ll check using Rack::Request the path of the current request and generate the desired response array with the status code, empty headers and a body of strings. Note that if the route is not /ping we simply call @app.call(env) letting the call go through the stack.

We could also read the request path like this: if env["PATH_INFO"] == "/ping" but I like using the Rack::Request interface.

Now let’s glue the middleware to an application.

Rack Builder

According to the documentation is:

Rack::Builder implements a small DSL to iteratively construct Rack applications

I can define this as the backbone that holds middlewares and applications together into a single Rack application. With that said, what we need to understand when we see Rack::Builder.new in an application, is that it’s a block that packs a bunch of middleware (defined by the use keyword) and an application at the bottom (defined by the run keyword) into a single Rack application app that will allow us to run the whole thing.

Let’s give it a look…

# config.ru
$:.unshift File.expand_path('../lib', __FILE__)

require 'custom_header'
require 'ping'

app = Rack::Builder.new do
  use Rack::CustomHeader
  use Rack::Ping
  run Proc.new { |env| [200, {}, ["Say something to me!"]] }
end

run app

The first thing we do is loading the lib directory into the $LOAD_PATH and require the two files that contain our middleware. Now that we know that our middleware is available, we’ll pack the application with Rack::Builder.new by adding the middleware on the top and the application at the bottom. The final step will be to run the new packed Rack app.

Let’s try this running the config.ru and doing some requests with cURL.

Start the application using the rackup command.

$ curl -i -X GET localhost:9292
HTTP/1.1 200 OK
X-Custom-Header: customheader.v1
Transfer-Encoding: chunked

Say something to me!

First we are trying the "/" route but it will be the same with any route as our application responds to all routes. The response is the expected, as there is our custom header and the application body.

But what happens if we use the /ping route:

$ curl -i -X GET localhost:9292/ping
HTTP/1.1 200 OK
X-Custom-Header: customheader.v1
Transfer-Encoding: chunked

pong

One thing to notice is that this response apart from being from the Ping middleware it also has our custom header, that’s because the CustomHeader middleware is before the Ping middleware and with middleware the order matters. This will not happen if the order was different, so be careful when you are setting up new middleware in the stack.


This is a very simple example that shows how middleware are created and work. I hope it’s useful for some of you.

Thanks for reading!

By