Rack middleware: co dzieje się przed twoim kontrolerem
Każde żądanie Rails przechodzi przez stos middleware Rack, zanim w ogóle dotrze do twojego kontrolera. Deep-dive o tym, czym jest Rack, jak działa stos middleware i jak napisać własne do logowania, autoryzacji i więcej.
Większość programistów Rails pracuje szczęśliwie latami, nigdy nie zastanawiając się,
co dzieje się przed dotarciem żądania do akcji kontrolera. Ale jest tam na dole cała
warstwa — Rack i stos middleware siedzący na nim — która obsługuje sesje, parsuje
parametry, ustawia ciasteczka i kilkanaście innych rzeczy, wszystko zanim twoje
def show w ogóle się uruchomi. Zrozumienie jej zamienia klasę „magii” w coś, co
możesz zobaczyć, debugować i rozszerzać. Oto czym jest Rack i jak go używać.
Rack: kontrakt pod każdą aplikacją webową Ruby
Rack to pięknie mała idea: standardowy interfejs między serwerami webowymi Ruby a
frameworkami webowymi Ruby. Aplikacja Rack to cokolwiek, co odpowiada na call,
bierze hash środowiska i zwraca tablicę trzech rzeczy: kodu statusu, hasha nagłówków i
ciała odpowiadającego na each.
class HelloApp
def call(env)
[200, { "Content-Type" => "text/plain" }, ["Hello from Rack"]]
end
end
To kompletna, uruchamialna aplikacja webowa. env to hash opisujący żądanie —
ścieżka, metoda, nagłówki, query string, wszystko. Wartość zwracana to odpowiedź. Ta
jedna konwencja jest powodem, dla którego Puma, Unicorn i WEBrick mogą uruchamiać
Rails, Sinatrę i twoją trzylinijkową aplikację zamiennie: wszystkie mówią Rackiem.
Rails sam, na dole, jest bardzo wyrafinowaną aplikacją Rack.
Middleware: stos opakowań
Middleware to naprawdę potężna część. Kawałek middleware Rack to obiekt, który
opakowuje inną aplikację Rack — bierze następną aplikację w konstruktorze, a jego
call może coś zrobić przed przekazaniem env w dół i coś z odpowiedzią w drodze w
górę:
class RequestTimer
def initialize(app)
@app = app
end
def call(env)
started = Time.now
status, headers, body = @app.call(env) # wywołaj resztę stosu
duration = ((Time.now - started) * 1000).round
headers["X-Runtime-Ms"] = duration.to_s
[status, headers, body]
end
end
Ponieważ każde middleware opakowuje następne, komponują się w cebulę: żądanie przechodzi do wewnątrz przez każdą warstwę do aplikacji w rdzeniu, a odpowiedź wraca na zewnątrz przez te same warstwy w odwrotnej kolejności. Każde middleware może zobaczyć i zmodyfikować żądanie w drodze do środka i odpowiedź w drodze na zewnątrz. To struktura, która pozwala przekrojowym sprawom — rzeczom, których potrzebuje każde żądanie — żyć w jednym miejscu zamiast być powtarzanym w każdym kontrolerze.
Rails to middleware aż do dołu
Powodem, dla którego to ma znaczenie dla programistów Rails, jest to, że ogromna część tego, co uważasz za „Rails”, jest faktycznie middlewarem. Możesz zobaczyć cały stos:
$ rake middleware
use Rack::Sendfile
use ActionDispatch::Static
use Rack::Runtime
use ActionDispatch::RequestId
use Rack::MethodOverride
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use Rack::Head
run YourApp::Application.routes
Czytanie tej listy jest pouczające. Ciasteczka, sesje, flash, parsowanie parametrów,
nadpisanie _method pozwalające formularzowi zrobić PATCH — nic z tego nie jest
funkcją kontrolera; to middleware, które już się uruchomiło, zanim twoja akcja się
wykona. Hash env dociera do twojego kontrolera wstępnie wypełniony przez wszystko
powyżej. Gdy raz przeczytasz rake middleware, sporo „magii” Rails zamienia się w „a,
to middleware Cookies”.
Pisanie i wstawianie własnego
Dodajesz middleware do stosu w config/application.rb, wybierając, gdzie siedzi
względem istniejących:
# config/application.rb
config.middleware.use RequestTimer
config.middleware.insert_before ActionDispatch::Cookies, MaintenanceMode
config.middleware.insert_after Rack::Runtime, RequestLogger
Pozycja ma znaczenie, bo determinuje, co już się uruchomiło. Middleware blisko góry uruchamia się pierwsze w drodze do środka i ostatnie w drodze na zewnątrz, i widzi najsurowsze żądanie; middleware blisko dołu uruchamia się tuż przed twoją aplikacją i widzi wszystko, co dodały górne warstwy. Kilka rzeczy, do których middleware jest właściwym narzędziem:
-
Tryb konserwacji. Sprawdź flagę i zwróć z góry 503, zanim żądanie w ogóle dotknie Railsów — bez kontrolera, bez bazy, po prostu płaska odpowiedź:
class MaintenanceMode def initialize(app); @app = app; end def call(env) if File.exist?("tmp/maintenance.txt") [503, { "Content-Type" => "text/html" }, ["<h1>Zaraz wracamy</h1>"]] else @app.call(env) end end end -
Przekrojowe logowanie, metryki i identyfikatory żądań, które powinny dotyczyć każdego żądania jednolicie, kontrolerów i assetów na równi.
-
Odrzucanie złego ruchu wcześnie — blokowanie IP, wymuszanie HTTPS, rate limiting (to dokładnie to, czym jest
Rack::Attack) — zanim zużyje jakiekolwiek zasoby aplikacji. -
Endpointy health-check, które muszą odpowiadać, nawet jeśli aplikacja za nimi się męczy, bo siedzą przed nią.
Zasada przewodnia: middleware jest do spraw dotyczących samego żądania i mających szerokie zastosowanie, zwłaszcza gdy chcesz je obsłużyć przed — albo zamiast — pełnym stosem Rails. Rzeczy potrzebujące twoich modeli domeny albo specyficzne dla jednej akcji należą do kontrolera; rzeczy dotyczące każdego żądania i chcące uruchomić się wcześnie należą do middleware.
Kiedy po nie nie sięgać
Middleware jest na tyle potężne, by je nadużyć. Logika biznesowa tam nie należy — nie ma czystego dostępu do twoich modeli ani kontekstu routingu i staje się trudna do testowania i znalezienia. Jeśli młodszy programista pójdzie szukać „co obsługuje zwroty”, spojrzy w kontrolery i usługi, nie w middleware Rack, więc umieszczenie tam logiki domeny ją ukrywa. Trzymaj middleware do naprawdę przekrojowych spraw na poziomie żądania i trzymaj ich liczbę umiarkowaną — każde middleware uruchamia się na każdym żądaniu, więc rozdęty stos to podatek od wszystkich.
Werdykt
Rack to jedna z najelegantszych abstrakcji w ekosystemie Ruby: trzyelementowy
kontrakt pozwalający serwerom, frameworkom i middleware współdziałać oraz model
stosu-opakowań czyniący przekrojowe sprawy komponowalnymi. Dla programisty Rails
zrozumienie tego opłaca się dwukrotnie — demistyfikuje dużą część tego, co framework
robi za ciebie, i daje ci czyste, potężne miejsce na obsługę rzeczy dotyczących
każdego żądania, zanim dotrą do twoich kontrolerów. Uruchom rake middleware,
przeczytaj stos, napisz jedno małe middleware, a warstwa, która kiedyś była
niewidoczna, staje się narzędziem, po które sięgasz świadomie.