Middleware in Rails – Full Guide with Real-World Examples and Secure API Encryption

Middleware in Rails – Full Guide with Real-World Examples & Secure API Decryption

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
1Rack::SendfileFile deliveryHandles efficient file serving (e.g., via NGINX or Apache).
2ActionDispatch::StaticServe static filesServes files from public/ directory if config.public_file_server.enabled = true.
3Rack::RuntimeResponse timingSets X-Runtime header with request duration.
4Rack::MethodOverrideHTTP method spoofingAllows method override via _method param (e.g., PUT, DELETE via POST).
5ActionDispatch::RequestIdRequest tracingAdds a unique X-Request-Id to every request for tracking.
6Rails::Rack::LoggerLoggingLogs request start/end info (used by Rails logger).
7ActionDispatch::ShowExceptionsFriendly error pagesRenders custom error pages instead of raw exceptions.
8ActionDispatch::DebugExceptionsDebuggingEnhances error reports in development (like stack traces).
9ActionDispatch::RemoteIpCorrect IP addressDetects real IP address behind proxies (uses X-Forwarded-For).
10ActionDispatch::ReloaderCode reloadingReloads app code between requests (in development).
11ActionDispatch::CallbacksLifecycle hooksRuns callbacks before/after request processing.
12ActiveRecord::Migration::CheckPendingMigration guardPrevents request if migrations are pending.
13ActionDispatch::CookiesCookie handlingManages incoming/outgoing cookies.
14ActionDispatch::Session::CookieStoreSession handlingStores session data in client-side cookies.
15ActionDispatch::FlashFlash messagesSupports flash[:notice], flash[:error], etc.
16Rack::HeadHEAD requests supportConverts HEAD requests to GET without body.
17Rack::ConditionalGetHTTP cachingHandles ETag and If-Modified-Since for caching.
18Rack::ETagResponse taggingAdds 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
1Centralized LogicMiddleware allows you to manage cross-cutting concerns (auth, logging, etc.) in one place instead of duplicating in every controller.
2Request PreprocessingModify, validate, or enrich requests before they reach the application logic.
3Response PostprocessingModify responses globally (e.g., adding headers, compressing content) after they are generated.
4ReusabilityMiddleware components can be reused across multiple applications or APIs.
5Modular DesignHelps separate responsibilities, making the codebase easier to maintain and test.
6Performance OptimizationTasks like static asset handling or short-circuiting invalid requests can be handled before hitting Rails controllers, saving resources.
7Third-party IntegrationEasily plug in tools like Rack::Cors, Rack::Attack, or JSON parsers into the stack.
8Early ExitCan return custom responses without loading the entire Rails stack (e.g., 401 for invalid token).

❌ Cons of Middleware in Rails

# Disadvantage Description
1Complex DebuggingBugs in middleware may not raise Rails-style errors, making them harder to trace and debug.
2Hard to Test in IsolationMiddleware often requires integration-style tests or mock env objects to test behavior.
3Order SensitivityIncorrect ordering in the middleware stack can break behavior (e.g., session access before cookies are parsed).
4Limited ContextMiddleware doesn’t have access to Rails-specific helpers, controllers, or sessions unless explicitly passed.
5Global ScopeMiddleware affects all requests, which may not be ideal for behavior that should be scoped to specific routes.
6Hidden LogicDevelopers unfamiliar with Rack may find it non-obvious where certain logic (like redirects or header injection) comes from.
7Memory Leaks RiskPoorly 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

Goal: Encrypt sensitive data on frontend with RSA, verify checksum, and decrypt securely in middleware.

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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top