Middleware in Rails – Full Guide with Real-World Examples and Secure API Encryption
Learn how middleware works in Ruby on Rails, when to use it, how to implement it, and how to securely decrypt encrypted payloads with RSA in a real-world API example.
What is Middleware in Rails?
In Rails, middleware is a layer that sits between the server (like Puma) and your application. It intercepts requests before they reach your Rails controllers and can modify requests or responses as needed. Rails uses Rack-based middleware, meaning every Rails app is also a Rack app. These middlewares are essentially callable Ruby objects that respond to .call(env) and return a status, headers, and body.
Why Do We Use Middleware?
- Add or modify HTTP headers.
- Log or audit requests globally.
- Handle authentication globally (e.g., API tokens).
- Manage session or cookies.
- Compress responses.
- Handle CORS.
- Redirect requests.
- Implement rate-limiting.
How Middleware Works
Every request goes through a stack of middleware before reaching the controller, and the response goes back through them in reverse order.
Think of it like a chain of filters:
Client ➡ Rack Middleware ➡ Rails Router ➡ Controller
⬅ (Response goes back)
📦 Built-in Middleware Stack in Rails
No. | Middleware Class | Purpose | Description |
---|---|---|---|
1 | Rack::Sendfile | File delivery | Handles efficient file serving (e.g., via NGINX or Apache). |
2 | ActionDispatch::Static | Serve static files | Serves files from public/ directory if config.public_file_server.enabled = true . |
3 | Rack::Runtime | Response timing | Sets X-Runtime header with request duration. |
4 | Rack::MethodOverride | HTTP method spoofing | Allows method override via _method param (e.g., PUT, DELETE via POST). |
5 | ActionDispatch::RequestId | Request tracing | Adds a unique X-Request-Id to every request for tracking. |
6 | Rails::Rack::Logger | Logging | Logs request start/end info (used by Rails logger). |
7 | ActionDispatch::ShowExceptions | Friendly error pages | Renders custom error pages instead of raw exceptions. |
8 | ActionDispatch::DebugExceptions | Debugging | Enhances error reports in development (like stack traces). |
9 | ActionDispatch::RemoteIp | Correct IP address | Detects real IP address behind proxies (uses X-Forwarded-For ). |
10 | ActionDispatch::Reloader | Code reloading | Reloads app code between requests (in development). |
11 | ActionDispatch::Callbacks | Lifecycle hooks | Runs callbacks before/after request processing. |
12 | ActiveRecord::Migration::CheckPending | Migration guard | Prevents request if migrations are pending. |
13 | ActionDispatch::Cookies | Cookie handling | Manages incoming/outgoing cookies. |
14 | ActionDispatch::Session::CookieStore | Session handling | Stores session data in client-side cookies. |
15 | ActionDispatch::Flash | Flash messages | Supports flash[:notice] , flash[:error] , etc. |
16 | Rack::Head | HEAD requests support | Converts HEAD requests to GET without body. |
17 | Rack::ConditionalGet | HTTP caching | Handles ETag and If-Modified-Since for caching. |
18 | Rack::ETag | Response tagging | Adds ETag headers for caching and comparison. |
You can see the entire stack with:
$ rails middleware
How to Implement Middleware in Rails
Step 1: Create a custom middleware class
# app/middleware/simple_logger.rb
class SimpleLogger
def initialize(app)
@app = app
end
def call(env)
Rails.logger.info "Request Path: #{env['PATH_INFO']}"
@app.call(env)
end
end
Step 2: Register the middleware in your Rails app
# config/application.rb
config.middleware.use "SimpleLogger" # Or: SimpleLogger if using class reference
10 Real-World Middleware Examples
- Logging all requests
- Rate limiting per IP
- Blocking IPs or regions
- Token authentication
- Maintenance mode
- Redirect HTTP to HTTPS
- Add CORS headers
- Strip trailing slashes
- Response compression
- Profiler/timer
10 Middleware Technical Q&A
# | Question | Answer |
---|---|---|
1 | What is the method signature required for a Rack middleware in Rails? | A middleware must implement a call(env) method that returns a [status, headers, body] array. |
2 | How can you insert a middleware at a specific point in the stack? | Use config.middleware.insert_before or insert_after in config/application.rb . |
3 | What does the env parameter in call(env) contain? |
It’s a Rack environment hash containing request data, headers, and server metadata. Example: env['PATH_INFO'] , env['rack.input'] . |
4 | How do you stop the middleware chain from continuing to the Rails app? | Return a response directly without calling @app.call(env) . This short-circuits the request. |
5 | How do you pass custom data between middlewares? | Store it in the env hash using a unique key, e.g., env['custom.user'] = user . |
6 | How can you view all middlewares currently active in a Rails app? | Run rails middleware from the terminal. |
7 | Can middleware modify both requests and responses? | Yes. Modify the env for the request and the [status, headers, body] array for the response. |
8 | How do you remove a middleware from the default stack? | Use config.middleware.delete(SomeMiddleware) in config/application.rb . |
9 | Can middleware be used to implement authentication logic? | Yes, especially for APIs. Example: Check Authorization header and return 401 Unauthorized if missing. |
10 | How can middleware handle exceptions raised in deeper layers? | Wrap @app.call(env) in a begin/rescue block and return a custom error response (e.g., 500 Internal Server Error ). |
Pros and Cons of Middleware
✅ Pros of Middleware in Rails
# | Advantage | Description |
---|---|---|
1 | Centralized Logic | Middleware allows you to manage cross-cutting concerns (auth, logging, etc.) in one place instead of duplicating in every controller. |
2 | Request Preprocessing | Modify, validate, or enrich requests before they reach the application logic. |
3 | Response Postprocessing | Modify responses globally (e.g., adding headers, compressing content) after they are generated. |
4 | Reusability | Middleware components can be reused across multiple applications or APIs. |
5 | Modular Design | Helps separate responsibilities, making the codebase easier to maintain and test. |
6 | Performance Optimization | Tasks like static asset handling or short-circuiting invalid requests can be handled before hitting Rails controllers, saving resources. |
7 | Third-party Integration | Easily plug in tools like Rack::Cors, Rack::Attack, or JSON parsers into the stack. |
8 | Early Exit | Can return custom responses without loading the entire Rails stack (e.g., 401 for invalid token). |
❌ Cons of Middleware in Rails
# | Disadvantage | Description |
---|---|---|
1 | Complex Debugging | Bugs in middleware may not raise Rails-style errors, making them harder to trace and debug. |
2 | Hard to Test in Isolation | Middleware often requires integration-style tests or mock env objects to test behavior. |
3 | Order Sensitivity | Incorrect ordering in the middleware stack can break behavior (e.g., session access before cookies are parsed). |
4 | Limited Context | Middleware doesn’t have access to Rails-specific helpers, controllers, or sessions unless explicitly passed. |
5 | Global Scope | Middleware affects all requests, which may not be ideal for behavior that should be scoped to specific routes. |
6 | Hidden Logic | Developers unfamiliar with Rack may find it non-obvious where certain logic (like redirects or header injection) comes from. |
7 | Memory Leaks Risk | Poorly implemented middlewares may retain references in the env hash, leading to memory leaks. |
When Not to Use Middleware
🚫 When You Shouldn’t Use Middleware in Rails
Situation | Why Middleware is Not Ideal | Better Alternative |
---|---|---|
Logic depends on session or flash | Middleware doesn’t always have access to session, flash, or controller context. | Use before_action in a controller. |
Feature only needed in one controller or route | Middleware runs globally for every request, which is unnecessary and inefficient for one-off logic. | Use controller filters (before_action ) or concern. |
Needs access to models or current user context | Middleware runs before controllers and often lacks full Rails context, including current_user or request params. |
Handle inside controller or service object. |
View-level changes or rendering needed | Middleware can’t render templates or use view helpers. | Use controller or custom view components. |
You need route-specific logic | Middleware does not discriminate by route by default—it runs for all requests. | Use route constraints or controller-level logic. |
Simple params validation or redirects | Middleware for simple validation can be overkill and hard to maintain. | Use before_action or validations inside controller. |
Highly interactive applications (e.g., Turbo, Hotwire) | Middleware won’t understand frontend-specific needs like Turbo Streams or Stimulus actions. | Handle on the client or in controllers. |
🔐 Advanced Middleware: Secure API Payload Decryption
Here’s a complete Rails + React secure encryption/decryption implementation where:
- Sensitive data is encrypted in React using RSA + checksum.
- Backend decrypts in a Rails middleware, not controller.
- Params are filtered from logs.
- Even if the public key is leaked, data can’t be decrypted or reused due to a checksum signature mechanism.
🔧 Step-by-Step Implementation
1. 🔑 Generate RSA Keys
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
Put them in:
- config/keys/private.pem
- config/keys/public.pem
2. 🧪 Backend-Generated Secret Key for Checksum
Generate a Rails secret and store it in credentials:
EDITOR="code --wait" rails credentials:edit
encryption:
secret_key: "YOUR_RANDOM_64_CHAR_SECRET"
3. 🔐 React Encryption + Checksum (Frontend)
Use JSEncrypt for RSA + SHA256 checksum:
import JSEncrypt from 'jsencrypt';
import CryptoJS from 'crypto-js';
async function encryptWithChecksum(cardData) {
const res = await fetch("/api/v1/public_key");
const { public_key, checksum_secret } = await res.json(); // checksum_secret = salt
const jsonData = JSON.stringify(cardData);
const checksum = CryptoJS.HmacSHA256(jsonData, checksum_secret).toString();
const payload = JSON.stringify({ data: cardData, checksum });
const encryptor = new JSEncrypt();
encryptor.setPublicKey(public_key);
const encrypted = encryptor.encrypt(payload);
return encrypted;
}
4. 📤 Rails API: Expose Public Key + Checksum Salt
# config/routes.rb
get '/api/v1/public_key', to: 'keys#show'
# app/controllers/api/v1/keys_controller.rb
class Api::V1::KeysController < ApplicationController
def show
render json: {
public_key: File.read(Rails.root.join('config/keys/public.pem')),
checksum_secret: SecureRandom.hex(8) # temporary salt per session/request
}
end
end
In production, use a temporary salt/token bound to the session/user so attackers can’t reuse the same checksum.
5. 🧩 Create a Rails Middleware for Decryption
# app/middleware/decrypt_card_payload.rb
class DecryptCardPayload
def initialize(app)
@app = app
end
def call(env)
req = Rack::Request.new(env)
if req.post? && req.content_type == 'application/json'
body = req.body.read
req.body.rewind
parsed = JSON.parse(body)
encrypted_payload = parsed["encrypted_payload"]
private_key = OpenSSL::PKey::RSA.new(File.read(Rails.root.join("config/keys/private.pem")))
decrypted_json = private_key.private_decrypt(Base64.decode64(encrypted_payload))
decrypted = JSON.parse(decrypted_json)
expected_checksum = OpenSSL::HMAC.hexdigest(
"SHA256",
Rails.application.credentials.dig(:encryption, :secret_key),
decrypted["data"].to_json
)
unless decrypted["checksum"] == expected_checksum
return [
400,
{ "Content-Type" => "application/json" },
[{ error: "Invalid checksum" }.to_json]
]
end
# Overwrite parsed input
env["action_dispatch.request.request_parameters"] ||= {}
env["action_dispatch.request.request_parameters"].merge!(decrypted["data"])
end
@app.call(env)
end
end
6. ✅ Register the Middleware
# config/application.rb
config.middleware.insert_before ActionDispatch::ParamsParser, DecryptCardPayload
7. 🕵️ Filter Sensitive Parameters from Logs
# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [
:card_number, :cvv, :expiry, :encrypted_payload
]
Final Thoughts
Middleware is a powerful layer in Rails for pre-processing requests and securing APIs. Using encryption with decryption middleware allows you to protect sensitive data without relying on controller logic.
Learn more about Rails setup