Skip to Content
Course content

28: Rack Middleware Concepts

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time in Rails, but for a long while, I treated the stuff happening "under the hood" as magic. Specifically, the way a request actually hits your code. In Ruby, that magic is almost always Rack. If you've ever looked at a config.ru file and wondered why it's there, you're looking at the entry point for Rack.

Instead of just reading the documentation, let's actually build something. I want to add a custom header to every single response in my application—something like X-Processed-By: MyCustomEngine. I could do this inside my controller, but that's tedious. I want it to happen automatically, regardless of which route is hit.

The simplest possible Rack app

First, I need a baseline. A Rack application is just an object that responds to call, takes an env hash, and returns an array with three specific elements: the status code, the headers, and the body. Let's start there.

# app.rb
class SimpleApp
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["Hello from the app!"]]
  end
end

# To run this, we'd use a config.ru file:
# run SimpleApp.new

If I run this with rackup, it works perfectly. But now I'm stuck. I want that custom header. If I add it inside SimpleApp, I'm just hardcoding it into the application logic. What if I have ten different apps? I don't want to rewrite that header logic ten times. I need a way to "wrap" the app.

Wrapping the application

I'll try creating a class that takes the app as an argument. I'll call it HeaderInjector. The idea is that the injector handles the request first, then passes it along to the actual app.

class HeaderInjector
  def initialize(app)
    @app = app
  end

  def call(env)
    # I'll just call the app and return the result
    @app.call(env)
  end
end

Right now, this does absolutely nothing. It's just a passthrough. But wait—the @app.call(env) line returns that three-element array (status, headers, body). Since I have that array in my hands before it goes back to the web server, I can actually mutate it.

Interacting with the response

Let's try to grab that headers hash and shove our custom key into it. This is where the "middleware" concept actually clicks: the middleware sits between the server and the app.

class HeaderInjector
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    
    # Here is the magic: we modify the headers hash before it leaves
    headers["X-Processed-By"] = "MyCustomEngine"
    
    [status, headers, body]
  end
end

I'll update my config.ru to use this:

# config.ru
require_relative 'app'

use HeaderInjector
run SimpleApp.new

When I run this and check the network tab in my browser (or use curl -I), there it is: X-Processed-By: MyCustomEngine. I didn't touch a single line of code in SimpleApp, yet the response changed. That's the power of middleware.

Thinking about the "Pipeline"

I started wondering: what if I want to do something before the app even sees the request? For example, maybe I want to log the request path. I can just add a line before the @app.call(env).

def call(env)
  puts "Incoming request to: #{env['PATH_INFO']}"
  
  status, headers, body = @app.call(env)
  headers["X-Processed-By"] = "MyCustomEngine"
  
  [status, headers, body]
end

Now I have a pipeline. The request flows down through the middleware (where I log the path), hits the app, and then the response flows back up through the middleware (where I inject the header) before finally hitting the user. If I add five different use statements in my config.ru, I've essentially built an onion. Each layer of middleware can inspect the request on the way in and tweak the response on the way out.

It's a remarkably simple pattern, but it's how almost every major Ruby framework handles things like sessions, cookies, and authentication. They aren't "features" of the framework's core logic; they're just layers of Rack middleware.




📋 Practical Task

Build a Request-Duration Logger Middleware

Your task is to create a Rack middleware called ResponseTimer. This middleware should calculate how long it takes for the application to process a request and then add that duration (in milliseconds) to a custom response header called X-Runtime-Ms.

  • Create a class ResponseTimer that follows the Rack middleware pattern (initialize and call).
  • Capture the start time before calling @app.call(env).
  • Capture the end time after the app returns the response.
  • Calculate the difference, convert it to milliseconds, and add it to the headers hash.
  • Ensure the middleware returns the standard Rack array: [status, headers, body].

Test your work: Use a simple Rack app and a config.ru file with use ResponseTimer to verify that the X-Runtime-Ms header appears in your HTTP response.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.