The real machine behind config.middleware
The last course's closing challenge asked you to write run_chain(env, middlewares, final_app). You start from final_app, walk the middleware list backward, and wrap each one around the current app. If your solution looked like middlewares.reverse_each { |mw| app = mw.new(app) }, you did more than pass the tests. You wrote the same line Rails runs on every boot, in production.
The real source
ActionDispatch::MiddlewareStack is the class config.middleware builds in the end. It lives in lib/action_dispatch/middleware/stack.rb inside actionpack-8.1.3.1, and it has a build method:
def build(app = nil, &block)
instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
middlewares.freeze.reverse.inject(app || block) do |a, e|
if instrumenting
e.build_instrumented(a)
else
e.build(a)
end
end
end
Set aside the instrumentation branch. It wraps each middleware in a class that fires an ActiveSupport::Notifications event around call, which helps with profiling but has nothing to do with the wrapping. What's left is middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }. That is the reverse-then-wrap shape of run_chain, written with inject instead of a loop that reassigns a variable.
What e.build(a) does
Each e here is an instance of Middleware, a small class inside MiddlewareStack, not your middleware class. It remembers the class, the constructor args, and the block you passed to use, insert_after and the rest:
class Middleware
attr_reader :args, :block, :klass
def initialize(klass, args, block)
@klass = klass
@args = args
@block = block
end
def build(app)
klass.new(app, *args, &block)
end
end
build(app) is one line: klass.new(app, *args, &block). For a plain config.middleware.use RequestTimingLogger, that's RequestTimingLogger.new(app), the same initialize(app) shape as in the last course. The *args, &block are there for calls that pass more. This app's config/initializers/omniauth.rb calls config.middleware.use OmniAuth::Builder do ... end, and the block goes straight into OmniAuth::Builder.new(app, &block).
Why the match matters
The match is the point of the last course's challenge. ActionDispatch::MiddlewareStack is not a black box. It's the small, readable idea you already built by hand, with a small Middleware class and an inject in place of a bare loop. Every time this app boots, in development or production, this build method runs once. It walks the app's config.middleware list backward (29 entries in development, 26 in production) and returns one nested object, with run NewbieOnRails::Application.routes at its center. There is no further layer underneath.
Key takeaways
ActionDispatch::MiddlewareStack#buildinactionpack-8.1.3.1comes down tomiddlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }: the same reverse-then-wrap shape as the previous course'srun_chainchallenge.- Each stack entry is a
Middlewareobject that remembers the class, the constructor args and the block. Itsbuild(app)is one line:klass.new(app, *args, &block). - Forwarding
*args, &blockis why this app'sconfig.middleware.use OmniAuth::Builder do ... endworks: the block given tousereaches the middleware's own constructor. - This runs once per boot on the app's stack (29 entries in development, 26 in production) and produces the nested chain
bin/rails middlewareprints. Nothing else is hidden beneath it.
Keep going with lesson 2
Recording, not applying: the MiddlewareStackProxy
Calls to config.middleware push lambdas onto arrays, and merge_into replays them late in boot against the default stack.
A free account also gives you the quizzes, your progress and notes, and the discussion.