Question

Rack Middleware in Ruby: A Beginner-Friendly Explanation

rubyruby-on-railshttpweb-applicationsrack

Question

What is Rack middleware in Ruby, and what does the term “middleware” mean in this context? Please explain how it fits into a Ruby web application and how it handles requests and responses.

Short Answer

Rack middleware is code that sits between a web server and your Ruby application. It can inspect, modify, reject, log, or otherwise handle an HTTP request before the application receives it, and it can modify the response before it is sent back to the client. By the end of this page, you will understand the Rack request/response flow, the middleware interface, and common uses in real Ruby applications.

Concept

Rack is a Ruby interface for web applications and web servers. It gives different Ruby web frameworks—such as Rails and Sinatra—a shared way to receive HTTP requests and return HTTP responses.

A Rack application is any Ruby object that responds to call. Rack calls that method with an environment hash named env:

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

The returned value has three parts:

  • status: an HTTP status code, such as 200 or 404.
  • headers: a hash of HTTP response headers.
  • body: an object that can be iterated to produce response content.

Middleware is a Rack application that wraps another Rack application. It receives the request first, can do work before calling the wrapped application, then receives that application's response and can do more work before returning it.

Conceptually:

Browser
  ↓ request
Web server
  ↓
Middleware A → Middleware B → Your application
  ↑                              ↓ response
Middleware A ← Middleware B ← Your application
  ↑
Browser

This design matters because cross-cutting jobs—work needed by many routes or pages—can be implemented once instead of copied into every controller or endpoint. Examples include logging, authentication, sessions, compression, CORS headers, error handling, and request timing.

Mental Model

Think of a Rack application as a shop and middleware as the staff stationed around its entrance and exit.

  • A customer (the HTTP request) arrives at the entrance.
  • A security guard may check identification before allowing entry.
  • A receptionist may write down the arrival time.
  • The customer reaches the shop (your application), which prepares an order (the response).
  • On the way out, a packer may add wrapping, and another staff member may record how long the visit took.

Each staff member can either:

  • allow the request to continue,
  • change information,
  • stop it early and provide a response, or
  • change the response on its way back.

A middleware stack is simply several of these wrappers placed around an application.

Take Quiz

Syntax and Examples

A Rack middleware class usually has two methods:

  1. initialize(app), which receives the next application in the stack.
  2. call(env), which processes the request and returns a Rack response.
class RequestLogger
  def initialize(app)
    @app = app
  end

  def call(env)
    puts "Request: #{env['REQUEST_METHOD']} #{env['PATH_INFO']}"

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

    puts "Response status: #{status}"
    [status, headers, body]
  end
end

@app is the next middleware or the final application. Calling @app.call(env) passes the request onward.

Here is a small Rack application and the logger wrapped around it:

class HelloApp
  def call(_env)
    [200, { 'Content-Type' => 'text/plain' }, ['Hello from Rack!']]
  end
end

app = RequestLogger.new(HelloApp.new)

When Rack calls app.call(env), RequestLogger runs first. It logs the request, calls HelloApp, logs the status returned by HelloApp, and returns the same response.

Middleware can also stop a request without calling @app. For example, this middleware blocks requests without an API key:

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

  def call(env)
    api_key = env['HTTP_X_API_KEY']

    unless api_key == 'expected-key'
      return [401, { 'Content-Type' => 'text/plain' }, ['Unauthorized']]
    end

    @app.call(env)
  end
end

The early return means the request never reaches the wrapped application.

Step by Step Execution

Consider this stack:

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

  def call(env)
    status, headers, body = @app.call(env)
    headers['X-Powered-By'] = 'Rack'
    [status, headers, body]
  end
end

class HelloApp
  def call(env)
    name = env['QUERY_STRING'].split('=').last
    message = name && !name.empty? ? "Hello, #{name}!" : 'Hello!'

    [200, { 'Content-Type' => 'text/plain' }, [message]]
  end
end

app = AddPoweredByHeader.new(HelloApp.new)

Suppose a browser requests /hello?name=Ada.

  1. Rack builds the env hash. It includes information such as the path, request method, headers, and query string.

  2. Rack calls app.call(env). Here, app is an AddPoweredByHeader object.

  3. AddPoweredByHeader#call calls @app.call(env).

  4. @app is the HelloApp object, so HelloApp#call runs.

  5. HelloApp reads env['QUERY_STRING'], creates Hello, Ada!, and returns:

    [200, { 'Content-Type' => 'text/plain' }, ['Hello, Ada!']]
    
  6. Control returns to AddPoweredByHeader#call.

  7. The middleware adds X-Powered-By: Rack to the response headers.

  8. It returns the final response to Rack.

  9. Rack and the web server send the status, headers, and body to the browser.

The request travels inward through the stack; the response travels outward through the same stack in reverse order.

Real World Use Cases

Rack middleware is especially useful for concerns that apply across many endpoints.

  • Request logging: Record HTTP methods, paths, status codes, and response times for debugging and monitoring.
  • Authentication: Verify a session, bearer token, or API key before protected endpoints run.
  • Authorization boundaries: Reject requests from disallowed IP addresses or require a specific role for a group of routes.
  • Session handling: Load session data from a cookie before the application runs and save it into the response afterward.
  • CORS: Add headers such as Access-Control-Allow-Origin so a browser frontend can call an API from another origin.
  • Error handling: Catch unexpected exceptions and return a controlled 500 Internal Server Error response.
  • Security headers: Add headers such as X-Content-Type-Options or Content-Security-Policy to every response.
  • Response compression: Compress suitable response bodies before sending them over the network.
  • Maintenance mode: Return a temporary 503 Service Unavailable response without changing every route.
  • Request IDs: Attach a unique request ID so logs from different services can be connected.
Take Quiz

Real Codebase Usage

In real applications, middleware is usually configured as a stack rather than manually instantiated in one file.

In a Rack configuration file, config.ru, use use to add middleware and run to specify the final application:

class HelloApp
  def call(_env)
    [200, { 'Content-Type' => 'text/plain' }, ['Hello!']]
  end
end

use Rack::ContentLength
use Rack::CommonLogger
run HelloApp.new

use wraps the final application. With multiple use calls, the first middleware is the outermost layer:

Rack::ContentLength
  → Rack::CommonLogger
    → HelloApp

In Rails, middleware is configured through the application configuration. For example:

# config/application.rb
config.middleware.use RequestLogger

Developers commonly follow these patterns:

  • Guard clauses: Check a condition and immediately return 401, 403, 404, or 429 instead of calling the next app.
  • Request context: Add values such as a request ID to env so later middleware, controllers, and logs can access them.
  • Response decoration: Call the next app first, then add headers, cookies, caching directives, or timing information.
  • Centralized error handling: Wrap the app with middleware that rescues expected errors and converts them to HTTP responses.
  • Careful ordering: Put middleware in an order that ensures prerequisites are available. For example, session middleware must run before code that reads the session.

A simple timing middleware looks like this:

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

  def call(env)
    started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    status, headers, body = @app.call(env)
    elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at

    headers['X-Response-Time'] = format('%.3f', elapsed)
    [status, headers, body]
  end
end

For production middleware, be careful when changing response bodies. Some Rack bodies are streaming or require cleanup, so middleware should preserve the Rack response contract and avoid consuming a body unless it handles that responsibility correctly.

Common Mistakes

Forgetting to call the next application

If middleware should allow the request through, it must call @app.call(env).

# Broken: every request ends here, and the real app never runs.
class BrokenLogger
  def initialize(app)
    @app = app
  end

  def call(env)
    puts env['PATH_INFO']
  end
end

Return the next application's response instead:

@ app.call(env)

The valid Ruby code has no space after @:

@app.call(env)

Returning the wrong response shape

Rack expects a three-element array: [status, headers, body].

# Broken
'Hello'
# Valid
[200, { 'Content-Type' => 'text/plain' }, ['Hello']]

Calling @app.call(env) twice

This can execute a controller action, database write, or other side effect twice.

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

Call it once, save the response if you need to modify it, and return it.

Modifying headers before calling the app, then losing them

The wrapped app may return a different headers hash. To reliably add a response header, modify headers after receiving the response:

status, headers, body = @app.call(env)
headers['X-Request-ID'] = 'abc123'
[status, headers, body]

Assuming middleware order does not matter

Order changes behavior. An authentication middleware cannot use session information until session middleware has made it available. Test middleware placement and inspect the configured stack when debugging.

Treating env like a clean, application-specific hash

env contains Rack and server data with established key conventions. Avoid overwriting standard keys. If you add your own value, use a clear namespaced key:

env['my_app.request_id'] = 'abc123'

Using wall-clock time for elapsed durations

For request duration, prefer a monotonic clock:

Process.clock_gettime(Process::CLOCK_MONOTONIC)

It is not affected by system clock adjustments.

Comparisons

ConceptWhat it doesBest use
Rack middlewareWraps an application and handles requests and/or responses for many endpointsLogging, sessions, authentication, headers, error handling
Route or controller actionHandles the business logic for one endpoint or resourceCreating an order, showing a profile, updating a record
Helper methodReuses small pieces of code when explicitly calledFormatting data, parsing a value, calculating a total
Web serverAccepts network connections and communicates with RackPuma, Unicorn, Passenger, WEBrick in development contexts
Reverse proxySits in front of the Ruby server at the network levelTLS termination, static files, load balancing; for example, Nginx

Middleware differs from controller code because it is typically global or stack-wide. A controller decides what /orders/42 should do; middleware handles concerns that may apply before or after many routes.

Middleware can be divided by when it does work:

Middleware styleTypical shapeExample
Request-onlyInspect env, then call the appLog method and path
Short-circuitingReturn early without calling the appReject a missing API key
Response-onlyCall app, then change responseAdd a security header
Around middlewareWork before and after calling appMeasure request duration
Take Quiz

Cheat Sheet

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

  def call(env)
    # Before the application

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

    # After the application

    [status, headers, body]
  end
end
  • A Rack app responds to call(env).

  • A Rack response is [status, headers, body].

  • env contains request and server information.

  • @app is the next item in the middleware stack.

  • Call @app.call(env) exactly once when forwarding a request.

  • Return early to block or handle a request yourself:

    return [403, { 'Content-Type' => 'text/plain' }, ['Forbidden']]
    
  • Add middleware in config.ru with use:

    use MyMiddleware
    run MyApp.new
    
  • The first use is the outermost middleware.

  • Add request-specific custom values to env with a namespaced key, such as env['my_app.request_id'].

  • Modify response headers after calling the next application.

FAQ

What does middleware mean in Rack?

It means a component placed between the web server and the final application. It can process a request before the application and a response after the application.

Is Rack middleware specific to Rails?

No. Rack is a Ruby web interface used by Rails, Sinatra, and other Ruby web applications. Middleware works with any Rack-compatible application.

Does every Rack middleware need an initialize method?

Most middleware classes need initialize(app) to store the next application. A middleware can accept additional configuration arguments, but it still needs access to the wrapped app.

What is env in Rack middleware?

env is a hash containing details about the current request and server environment, such as REQUEST_METHOD, PATH_INFO, request headers, and Rack-specific values.

Can middleware stop a request?

Yes. It can return its own [status, headers, body] response without calling @app.call(env). This is common for authentication, rate limiting, and maintenance mode.

In what order does Rack middleware run?

Requests enter from the outermost middleware toward the app. Responses return from the app back through those middleware layers in reverse order.

Where do I add middleware in a Rails application?

Rails applications configure it through config.middleware, commonly in config/application.rb or an environment configuration file.

Why should I not put all logic in middleware?

Middleware is best for reusable HTTP-level concerns. Endpoint-specific business rules usually belong in routes, controllers, service objects, or domain code.

Take Quiz

Mini Project

Description

Build a small Rack application protected by middleware. The middleware will assign every request a unique ID, log the request, and add that ID to the response headers. Request IDs are useful when matching a client-reported problem to server logs.

Goal

Create a Rack middleware stack that logs each request and returns an X-Request-ID header with every response.

Requirements

Create a Rack application that returns a plain-text welcome message. Create middleware that generates a unique request ID. Store the request ID in env using a namespaced key. Log the request method, path, and request ID. Add the same request ID to the response as X-Request-ID.

Take Quiz

Keep learning

Related questions

View more