O que é o Rack

Prévia grátis 5 min de leitura

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 (o Gemfile.lock fixa rack (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 body responde a each e produz pedaços de String. O Rack 3 também aceita um body que responda a call(stream) para streaming, mas o each é 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; o status é um Integer; o headers tem nomes em minúsculas e valores String, ou um Array de Strings para vários valores; o body responde a each.
  • Em desenvolvimento, o bin/rails middleware desta aplicação mostra 29 linhas use antes da única linha run, 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.
Aula 1 de 7 · grátis

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.

Digite para buscar...