35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Custom Rack Middleware in Rails 8: Een Praktische Gids met Concrete Voorbeelden

Custom Rack Middleware in Rails 8: Een Praktische Gids met Concrete Voorbeelden

Roger Heykoop
Ruby on Rails
Hoe je custom Rack middleware schrijft, test en deployt in Rails 8. Met voorbeelden voor request timing, tenant detectie, request ID propagatie en de middleware stack volgorde waar iedereen over struikelt.

Elk Rails-request passeert een stack van middleware voordat het je controller bereikt. De meeste developers denken nooit na over deze stack totdat er iets misgaat — een ontbrekend request ID in logs, CORS headers die verdwijnen in productie, of response times die zonder verklaring omhoogschieten.

Rails 8 wordt geleverd met ongeveer 25 middleware standaard. Je kunt de jouwe nu bekijken:

bin/rails middleware

Elke middleware is een simpel Ruby-object dat een request ontvangt, het optioneel aanpast, het doorgeeft aan de stack, en optioneel de response aanpast op de terugweg. Dat is alles. Geen magie, geen framework — gewoon de Rack-interface.

De Rack-interface in 30 seconden

Een Rack middleware is elk object dat reageert op call(env) en een array met drie elementen retourneert: [status, headers, body]. Dit is het skelet:

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

  def call(env)
    # Voordat het request Rails bereikt
    status, headers, body = @app.call(env)
    # Nadat de response terugkomt
    [status, headers, body]
  end
end

Het app-argument is de volgende middleware in de stack (of je Rails-app helemaal onderaan). Je roept het aan, krijgt de response, en retourneert het. Dat is het hele contract.

Voorbeeld 1: Request timing middleware

Laten we iets nuttigs bouwen. Deze middleware meet hoe lang elk request duurt en voegt de timing toe als response header:

# lib/middleware/request_timer.rb
class RequestTimer
  def initialize(app)
    @app = app
  end

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

    headers["X-Request-Time"] = format("%.4f", elapsed)
    [status, headers, body]
  end
end

Registreer het in config/application.rb:

require_relative "../lib/middleware/request_timer"

module MyApp
  class Application < Rails::Application
    config.middleware.use RequestTimer
  end
end

Process.clock_gettime(Process::CLOCK_MONOTONIC) gebruiken in plaats van Time.now maakt hier uit. Monotone klokken worden niet beïnvloed door NTP-aanpassingen of systeemklokwijzigingen, dus je timings blijven nauwkeurig, zelfs tijdens zomer-/wintertijdovergangen.

Verifieer dat het werkt:

curl -I http://localhost:3000/
# X-Request-Time: 0.0234

Voorbeeld 2: Tenant detectie voor multi-tenant apps

In een multi-tenant SaaS-app moet je vaak de tenant identificeren voordat er controllercode draait. Middleware is de juiste plek hiervoor:

# lib/middleware/tenant_resolver.rb
class TenantResolver
  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env)
    host = request.host

    tenant = Tenant.find_by(domain: host)

    if tenant
      env["app.current_tenant"] = tenant
      @app.call(env)
    else
      [404, { "content-type" => "text/plain" }, ["Unknown tenant"]]
    end
  end
end

Je controllers kunnen de tenant dan benaderen via request.env["app.current_tenant"]. Dit draait vóór routing, vóór authenticatie, vóór alles — precies wat je wilt voor tenant-isolatie.

Als je een multi-tenant Rails-applicatie bouwt, houdt tenant-resolutie in middleware het uit ApplicationController-callbacks waar de executievolgorde moeilijker te garanderen is.

Waar je middleware in de stack hoort

Hier gaat het mis bij veel developers. De volgorde maakt uit, en Rails geeft je vier methoden om het te bepalen:

# Voeg toe aan het einde van de stack
config.middleware.use MyMiddleware

# Voeg toe vóór een specifieke middleware
config.middleware.insert_before ActionDispatch::Static, MyMiddleware

# Voeg toe ná een specifieke middleware
config.middleware.insert_after Rails::Rack::Logger, MyMiddleware

# Vervang een bestaande middleware
config.middleware.swap ActionDispatch::ShowExceptions, MyCustomExceptions

Een veelgemaakte fout: timing middleware plaatsen na ActionDispatch::Static. Statische bestandsrequests worden afgehandeld voordat je middleware ze ziet, dus je timingnummers missen die requests. Wil je alles timen, plaats dan vóór ActionDispatch::Static:

config.middleware.insert_before ActionDispatch::Static, RequestTimer

Draai bin/rails middleware na wijzigingen om te verifiëren dat je middleware op de verwachte plek staat.

Voorbeeld 3: Request ID propagatie voor distributed tracing

Rails 8 bevat standaard ActionDispatch::RequestId, die een UUID genereert per request. Maar in microservice-architecturen wil je een bestaand request ID van upstream services propageren:

# lib/middleware/request_id_propagator.rb
class RequestIdPropagator
  HEADER = "X-Request-Id"

  def initialize(app)
    @app = app
  end

  def call(env)
    request_id = env["HTTP_X_REQUEST_ID"] || SecureRandom.uuid
    env["HTTP_X_REQUEST_ID"] = request_id

    status, headers, body = @app.call(env)
    headers[HEADER] = request_id
    [status, headers, body]
  end
end

Voeg dit toe vóór ActionDispatch::RequestId zodat Rails je gepropageerde ID oppikt in plaats van een nieuwe te genereren:

config.middleware.insert_before ActionDispatch::RequestId, RequestIdPropagator

Dit werkt goed samen met gestructureerde logging — zodra elke logregel het request ID bevat, wordt het traceren van een request over services heen eenvoudig.

Middleware testen in isolatie

Je hoeft Rails niet op te starten om middleware te testen. Rack middleware is gewoon Ruby:

# test/middleware/request_timer_test.rb
require "test_helper"
require "rack/test"

class RequestTimerTest < ActiveSupport::TestCase
  include Rack::Test::Methods

  def app
    inner_app = ->(env) { [200, {}, ["OK"]] }
    RequestTimer.new(inner_app)
  end

  test "voegt X-Request-Time header toe" do
    get "/"
    assert last_response.headers.key?("X-Request-Time")
    assert_match(/\d+\.\d{4}/, last_response.headers["X-Request-Time"])
  end

  test "geeft status en body door" do
    get "/"
    assert_equal 200, last_response.status
    assert_equal "OK", last_response.body
  end
end

De inner_app lambda fungeert als stand-in voor de rest van je applicatie. Geen database, geen routes, geen Rails boot — tests draaien in milliseconden.

Response bodies correct afhandelen

Een valkuil die mensen in productie bijt: Rack response bodies moeten reageren op each. Als je middleware de body aanpast, moet je dit goed afhandelen:

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

  # Fout: body kan een Rack::BodyProxy zijn, geen string
  # modified = body + "<script>...</script>"

  # Goed: verzamel de body eerst
  response_body = []
  body.each { |chunk| response_body << chunk }
  body.close if body.respond_to?(:close)

  modified = response_body.join
  modified.gsub!("</body>", "<script>...</script></body>")

  headers["content-length"] = modified.bytesize.to_s
  [status, headers, [modified]]
end

Roep altijd body.close aan als de body erop reageert. Dit overslaan lekt file descriptors wanneer Rails responses streamt vanaf schijf.

Performanceoverwegingen

Middleware draait bij elk request. Een middleware die 1ms kost voegt 1ms toe aan elke pageload, elke API-call, elke health check. Dat telt snel op.

Regels die ik volg in productie:

  • Geen database queries in middleware tenzij absoluut noodzakelijk (tenant-resolutie is een uitzondering). Gebruik caching als je moet querien.
  • Geen zware berekeningen. Als je request bodies moet parsen, doe het dan lazy.
  • Short-circuit vroeg. Als je middleware alleen geldt voor bepaalde paden, check het pad eerst en roep @app.call(env) direct aan voor niet-matchende requests.
def call(env)
  return @app.call(env) unless env["PATH_INFO"].start_with?("/api")

  # Dure logica alleen voor API-requests
  # ...
end

Als je onverklaarde response time-stijgingen ziet, kan Rails performance profiling met YJIT helpen identificeren of middleware de boosdoener is.

Conditionele middleware in Rails 8

Soms wil je middleware alleen in bepaalde omgevingen. Rails maakt dit netjes:

# config/environments/development.rb
config.middleware.use WebConsole::Middleware

# config/environments/production.rb
config.middleware.use RateLimiter, requests_per_minute: 60

Je kunt ook config.middleware.delete gebruiken om standaard middleware te verwijderen die je niet nodig hebt:

# Als je API-only bent en geen statische bestanden serveert
config.middleware.delete ActionDispatch::Static

# Als je cookies anders afhandelt
config.middleware.delete ActionDispatch::Cookies

Voor API-only Rails-apps (config.api_only = true) verwijdert Rails al diverse middleware (cookies, session, flash). Check wat er overblijft met bin/rails middleware voordat je meer toevoegt.

Wanneer je middleware niet moet gebruiken

Middleware is niet altijd het antwoord. Gebruik een controller concern of before_action wanneer:

  • De logica alleen geldt voor specifieke controllers of actions
  • Je toegang nodig hebt tot Rails routing-informatie (params, current user)
  • Het gedrag afhangt van het response content type

Gebruik middleware wanneer:

  • De logica moet draaien voor elk request ongeacht de route
  • Je werkt op HTTP-niveau (headers, timing, request IDs)
  • Je wilt dat de logica draait vóór Rails routing
  • Je responses moet aanpassen nadat de controller klaar is

Als je authenticatie- of autorisatiecontroles toevoegt, horen die meestal in controllers of een dedicated authenticatielaag, niet in middleware. Middleware kan niet makkelijk je User-model of sessie benaderen — het draait te vroeg in de stack.

FAQ

Hoeveel middleware is te veel in een Rails-app?

Er is geen harde limiet, maar elke middleware voegt latency toe. Rails 8 wordt geleverd met ongeveer 25 standaard, en de meeste productie-apps voegen er 2-5 custom bij. Als je boven de 10 custom middleware zit, overweeg dan of sommige logica in controllers thuishoort. Profileer met bin/rails middleware en meet de request-overhead.

Kan middleware de request body aanpassen voordat het de controller bereikt?

Ja. Je kunt env["rack.input"] lezen en vervangen door een nieuw StringIO-object. Zo werkt request body encryption middleware. Let op: env["rack.input"] moet reageren op read, rewind en gets. Vervang het door een StringIO die je aangepaste content wrapt.

Wat is het verschil tussen Rack middleware en Rails middleware?

Rack middleware volgt de Rack-specificatie — elk Ruby-object met call(env) dat [status, headers, body] retourneert. Rails middleware is gewoon Rack middleware die geregistreerd wordt via de Rails-configuratie. Rails voegt gemak toe (de config.middleware API, omgevingsspecifieke stacks), maar de onderliggende interface is puur Rack.

Hoe debug ik de middleware-executievolgorde?

Draai bin/rails middleware om de volledige stack te zien. Voor runtime debugging, voeg een simpele logger toe aan het begin en einde van je call-methode: Rails.logger.debug "#{self.class.name} START". Je ziet het nesting-patroon — requests stromen naar beneden door de stack en responses stromen weer omhoog.

Werkt middleware met Rails Action Cable en WebSockets?

Action Cable heeft zijn eigen middleware-stack, gescheiden van de HTTP-stack. Je HTTP middleware draait standaard niet voor WebSocket-verbindingen. Als je middleware nodig hebt voor WebSocket-verbindingen, configureer het dan via config.action_cable.middleware of bekijk hoe Solid Cable WebSocket-verbindingen afhandelt.

#rails 8 #rack #middleware #performance #ruby
R

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation