O que é o Rack
Uma corrida de revezamento tem vários corredores, e não um só. Cada um corre sua parte e passa o bastão ao próximo. Da arquibancada parece uma corrida contínua. De perto é uma cadeia de passagens, e qualquer corredor pode atrasar o bastão, acelerá-lo ou se recusar a passá-lo. Uma requisição no Rails funciona do mesmo jeito. "O Rails" não é uma coisa só processando sua requisição. É uma cadeia de objetos pequenos que passam a requisição adiante, e o último da cadeia chama suas rotas.
O contrato: um método, três coisas de volta
O Rack é a biblioteca por baixo de toda aplicação Rails, e o contrato dele é pequeno de propósito. Qualquer objeto que responda a call(env) e devolva [status, headers, body] é uma aplicação Rack válida:
- O
envé um Hash com a requisição inteira:env["REQUEST_METHOD"]("GET","POST"...),env["PATH_INFO"]("/catalog/roadmap"),env["rack.input"](o corpo da requisição, como um objeto parecido com IO), e dezenas de outras chaves que os middlewares anteriores acrescentaram. - O
statusé um Integer (200,404,302). - O
headersé um Hash de nome de cabeçalho para valor. Esta aplicação usa o Rack 3 (oGemfile.lockfixarack (3.2.5)). No Rack 3, os nomes de cabeçalho precisam estar em minúsculas, e o valor é uma String ou, para um cabeçalho com vários valores, um Array de Strings. Um valor com quebra de linha é inválido. - O
bodyresponde aeache produz pedaços de String. O Rack 3 também aceita um body que responda acall(stream)para streaming, mas oeaché a forma que você vai escrever à mão.
Essa é a interface inteira. Não há superclasse obrigatória; respond_to?(:call) e a forma certa de retorno bastam. O próprio Rails.application é um objeto que cumpre esse contrato, pelo Rails::Engine#call. Ele rotear para seus controllers é um detalhe de como responde a call(env), e não uma interface diferente.
O Rack::Lint, que confere uma aplicação contra a especificação, mostra a regra dos cabeçalhos:
{ "x-a" => "1\n2" } -> Rack::Lint::LintError: invalid header value x-a: "1\n2"
{ "x-a" => ["1", "2"] } -> passes
Sua aplicação é um stack
Rode bin/rails middleware nesta aplicação, em desenvolvimento, e você recebe a cadeia que ela monta em todo boot:
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use Propshaft::Server
use ActionDispatch::Executor
use ActionDispatch::ServerTiming
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use Rack::Attack
use OmniAuth::Builder
run NewbieOnRails::Application.routes
São 29 linhas use antes da única linha run, que despacha para um controller. Cada uma é uma pequena aplicação Rack envolvendo a próxima. O HostAuthorization chama o Sendfile, que chama o Static, que chama o Propshaft::Server, e assim por diante até o run, a única linha da cadeia que não envolve nada. O ActionDispatch::Session::CookieStore lê e escreve o cookie que sua aplicação chama de sessão. O ActionDispatch::Flash é o que faz o flash[:notice] sobreviver a um redirect. Nenhum dos dois está embutido no núcleo do Rails; os dois são middleware, nesta lista e nesta ordem, cada um fazendo um trabalho e passando adiante.
O stack de produção é menor. Ele tira os auxiliares de desenvolvimento, como o WebConsole::Middleware, o ActionDispatch::Reloader e o ActiveRecord::Migration::CheckPending, e acrescenta o ActionDispatch::AssumeSSL e o ActionDispatch::SSL perto do topo.
Por que a ordem nessa lista importa
Cada middleware só vê o que os anteriores passaram adiante, e só age antes de os seguintes rodarem. O ActionDispatch::Flash guarda as mensagens na sessão, então precisa ficar abaixo do ActionDispatch::Session::CookieStore; acima dele, não haveria sessão para ler. O ActionDispatch::RequestId roda antes do Rails::Rack::Logger, e é por isso que o log de produção consegue marcar cada linha com o id da requisição. A posição decide se um middleware funciona ou nunca vê o dado de que precisa. O resto deste curso trata de escrever seu próprio elo nessa cadeia: onde ele entra, o que pode fazer com uma requisição na ida e com uma resposta na volta, e como testá-lo sem subir um servidor.
Pontos-chave
- O contrato inteiro do Rack é um método:
call(env)na entrada,[status, headers, body]na saída. Não é preciso classe base, só a forma certa. - O
envé um Hash com a requisição; ostatusé um Integer; oheaderstem nomes em minúsculas e valores String, ou um Array de Strings para vários valores; obodyresponde aeach. - Em desenvolvimento, o
bin/rails middlewaredesta aplicação mostra 29 linhasuseantes da única linharun, que despacha para suas rotas. O Rails é um stack, e não um monólito. - A ordem nesse stack tem função: um middleware só usa o que os de cima já puseram no
env, e só age antes de os de baixo rodarem.
Continue com a aula 2
Escrevendo um middleware: initialize e call
Monta um middleware de medição de tempo com initialize e call, e mostra o que quebra sem o @app.call(env).
Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.