Understanding Rails Middleware
Middleware is the bridge between your application and the HTTP requests it receives. Every request that hits your Rails application passes through a series of middleware layers before reaching your controller, and every response passes through those same layers on its way back out. Understanding how to use and create middleware will give you powerful control over request processing in your applications.
In this guide, you’ll learn what middleware is in the Rails context, how the middleware stack works, how to build custom middleware, and when to use it versus other approaches like controllers or concerns.
What Is Middleware?
Rails inherits its middleware architecture from Rack, a specification that standardizes how Ruby web frameworks interact with web servers. A middleware is simply a component that sits between the server and your application, able to inspect and modify both incoming requests and outgoing responses.
When a request arrives, it enters the middleware stack at the top and flows downward through each middleware layer. Each middleware can choose to pass the request to the next layer, return a response immediately without forwarding, or modify the request before passing it along. The same happens in reverse for the response.
Rails ships with numerous built-in middleware components. ActionDispatch::Static serves static files. ActionDispatch::Cookies handles cookie serialization. ActionDispatch::Session manages session data. ActionDispatch::ShowExceptions catches errors and renders friendly error pages. Rack::Runtime adds an X-Runtime header showing request duration. These work together to handle cross-cutting concerns that would be messy to implement in controllers.
The Rails Middleware Stack
You can inspect your application’s middleware stack using the Rails rake task:
rails middleware
You’ll see something like this:
use ActionDispatch::Static
use ActionDispatch::Executor
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use ActionDispatch::Static
use ActionDispatch::Files
use ActionDispatch::Executor
use ActionDispatch::Static
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run MyApp::Application.routes
The order matters significantly. ShowExceptions wraps everything below it, so any exceptions from later middleware get caught and displayed. Cookies must come before Session because sessions rely on cookies. Understanding this order helps you debug issues and place your own middleware correctly.
Creating Custom Middleware
Creating your own middleware is straightforward. A middleware is simply a class that responds to call(env), where env is a hash containing request information. The method returns an array of three elements: status code, headers hash, and response body.
Here’s a simple middleware that adds a custom header to every response:
class AddTimestampHeader
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
headers['X-Generated-At'] = Time.now.iso8601
[status, headers, body]
end
end
To add this middleware to your application, create a file in app/middleware/ and then configure it in config/application.rb:
module MyApp
class Application < Rails::Application
config.middleware.use AddTimestampHeader
end
end
The use method adds middleware to the stack. By default, it places the middleware at the bottom of the stack, just before your routes. You can insert middleware at specific positions using insert_before, insert_after, or prepend.
Middleware for Request Logging
A practical use case is logging all requests with timing information. This helps you identify slow requests and unusual traffic patterns:
class RequestLogger
def initialize(app, logger = Rails.logger)
@app = app
@logger = logger
end
def call(env)
request = Rack::Request.new(env)
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@logger.info "Started #{request.request_method} #{request.path} " \
"from #{request.remote_ip}"
status, headers, body = @app.call(env)
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
@logger.info "Completed #{status} in #{'%.2f' % duration}ms"
[status, headers, body]
end
end
This middleware measures actual processing time using monotonic clock time, which is more reliable than wall clock time for performance measurement. It logs the HTTP method, path, client IP, response status, and duration.
Conditional Middleware
Sometimes you only want middleware to run for certain requests. You can wrap your middleware logic with conditionals:
class AdminOnlyMiddleware
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
if request.path.start_with?('/admin')
# Admin-specific logic here
# This runs before the rest of the stack
end
@app.call(env)
end
end
Alternatively, use Rails’ built-in middleware configuration to apply conditions:
config.middleware.use AdminOnlyMiddleware, only: ['/admin', '/admin/*']
config.middleware.use AdminOnlyMiddleware, except: ['/api/*']
This approach keeps your middleware focused while allowing flexible application.
When to Use Middleware
Middleware excels at handling cross-cutting concerns that affect many or all requests: authentication, logging, request timing, header manipulation, cookies, sessions, and CORS. If something needs to happen for nearly every request, middleware is usually the right place.
However, middleware isn’t always the best solution. For controller-specific concerns, use filters or concerns. For model-specific logic, keep it in the model. For view-related processing, consider helpers or view decorators. Middleware should handle infrastructure-level concerns, not business logic.
One common mistake is putting too much logic in middleware, which makes debugging difficult since the stack order determines execution. If your middleware is doing complex work, consider whether that logic belongs elsewhere.
Debugging Middleware Issues
When things go wrong with middleware, the stack trace alone won’t tell you much because middleware executes before your controllers are even invoked. Here are some debugging strategies:
First, use rails middleware to verify your middleware is actually loaded and in the right position. Second, temporarily disable other middleware to isolate issues:
# Disable all middleware except yours
config.middleware = ActionDispatch::MiddlewareStack.new do |m|
m.use YourMiddleware
end
Third, add logging directly in your middleware to trace request flow:
def call(env)
Rails.logger.debug "Middleware entering: #{env['PATH_INFO']}"
result = @app.call(env)
Rails.logger.debug "Middleware exiting: #{result[0]}"
result
end
See Also
- Rails Routing — How Rails routes map requests to controllers
- ActiveRecord Basics — Database interaction in Rails
- ActionCable and WebSockets — Real-time communication in Rails