A máquina real por trás do config.middleware

Prévia grátis 3 min de leitura

O desafio final do curso passado pedia uma função run_chain(env, middlewares, final_app). Ela começa por final_app, percorre a lista de middlewares de trás para frente e envolve cada um em volta da aplicação atual. Se a sua solução ficou parecida com middlewares.reverse_each { |mw| app = mw.new(app) }, ela fez mais do que passar nos testes. Você escreveu a mesma linha que o Rails roda a cada boot, em produção.

O código-fonte

ActionDispatch::MiddlewareStack é a classe que o config.middleware monta no fim. Ela fica em lib/action_dispatch/middleware/stack.rb, dentro da actionpack-8.1.3.1, e tem um método build:

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

Deixe de lado o branch de instrumentação. Ele envolve cada middleware numa classe que dispara um evento de ActiveSupport::Notifications em volta de call. Isso ajuda no profiling, mas não tem nada a ver com o encadeamento. O que sobra é middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }: a mesma ideia de inverter a lista e envolver do run_chain, escrita com inject no lugar de um loop que reatribui uma variável.

O que e.build(a) faz

Cada e aqui é uma instância de Middleware, uma classe pequena dentro de MiddlewareStack, e não a sua classe de middleware. Ela guarda a classe, os argumentos do construtor e o bloco passado para use, insert_after e os demais:

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) tem uma linha só: klass.new(app, *args, &block). Num config.middleware.use RequestTimingLogger simples, isso vira RequestTimingLogger.new(app), o mesmo initialize(app) do curso passado. O *args, &block serve para as chamadas que passam mais coisas. O config/initializers/omniauth.rb desta aplicação chama config.middleware.use OmniAuth::Builder do ... end, e o bloco segue direto para OmniAuth::Builder.new(app, &block).

Por que essa coincidência importa

Chegar ao mesmo código era o objetivo do desafio do curso passado. ActionDispatch::MiddlewareStack não é uma caixa-preta. É a ideia pequena e legível que você já construiu à mão, com uma classe pequena Middleware e um inject no lugar do loop. Sempre que esta aplicação sobe, em desenvolvimento ou em produção, esse build roda uma vez. Ele percorre de trás para frente a lista do config.middleware (29 entradas em desenvolvimento, 26 em produção) e devolve um único objeto aninhado, com run NewbieOnRails::Application.routes no centro. Não há outra camada por baixo.

Pontos-chave

  • ActionDispatch::MiddlewareStack#build, na actionpack-8.1.3.1, se resume a middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }: a mesma lógica de inverter e envolver do desafio run_chain do curso passado.
  • Cada entrada da pilha é um objeto Middleware que guarda a classe, os argumentos do construtor e o bloco. O build(app) dela tem uma linha: klass.new(app, *args, &block).
  • É por repassar *args, &block que o config.middleware.use OmniAuth::Builder do ... end desta aplicação funciona: o bloco dado ao use chega ao construtor do próprio middleware.
  • Isso roda uma vez por boot, na pilha da aplicação (29 entradas em desenvolvimento, 26 em produção), e gera a cadeia aninhada que o bin/rails middleware mostra. Não há mais nada escondido por baixo.
Aula 1 de 7 · grátis

Continue com a aula 2

Gravar em vez de aplicar: o MiddlewareStackProxy

As chamadas a config.middleware guardam lambdas em arrays, e o merge_into as aplica no fim do boot sobre a pilha padrão.

Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.

Digite para buscar...