Препарируем Rack
Давайте поговорим немного о Rack. Rack - это важная часть Ruby веб-стека и, де-факто, стандарт общения между веб-сервером и приложением. Все Ruby-сервера, например Puma, Unicorn или Thin, передают HTTP запросы приложению и получают обратно ответы именно по Rack интерфейсу. Обычно, правда, приложение ничего об этом не знает - обо всем уже позаботился какой-нибудь фреймворк вроде Rails или Sinatra.
Но если Rack это просто спецификация, контракт между сервером и
приложением, тогда зачем нужен еще и gem rack
? Какая у него роль и
почему его подключают веб-сервера и фреймворки? Давайте разбираться. Но
сначала немного матчасти.
Содержание
Спецификация Rack
Rack-интерфейс на самом деле довольно прост. Приложение - это объект с
методом call
. Метод call
принимает HTTP-запрос и возвращает
HTTP-ответ. Веб-сервер отдает запрос приложению в виде Hash
‘а, который
по конвенции называют env
. Ответ должен быть в виде массива из трех
элементов - статус (200 или 500, к примеру), заголовки и тело ответа.
Заголовки возвращаются в виде списка пар имя-значение, а тело ответа -
это список строк (такой подход с телом запроса иногда нужен, чтобы
стримить данные - отправлять их клиенту по частям).
Полную спецификацию можно найти здесь.
Пример middleware
Давайте напишем простое Rack-приложение.
Мы создадим файл config.ru
- это стартовая точка любого
Rack-приложения. Имя файла может быть любым, но по конвенции веб-сервер
ожидает найти именно такой файл. Определим наше приложение в виде
lambda. У нее как у любого объекта класса Proc
есть метод call
и
поэтому она подходит для нашей цели:
app = lambda do |env|
[200, { 'Content-Type' => 'text/plain' }, ['Hello']]
end
run app
Это приложение на любой запрос возвращает статус 200, строчку ‘Hello’ и
заголовок ‘Content-Type’. Запустить это приложение можно в консоле
командой rackup
, которая идет в составе gem‘а:
$ rackup --port 5000
Если выполнить следующий HTTP запрос в консоли командой curl
, то
приложение, ожидаемо, ответит нам “Hello”:
$ curl http://0.0.0.0:5000/foo/bar\?q\=qwerty -d 'Hi'
Hello
А теперь посмотрим, как же выглядит HTTP запрос:
{
"rack.version"=>[1, 3],
"rack.errors"=>"#<Rack::Lint::ErrorWrapper:0x00007fc0e2166a98>",
"rack.multithread"=>true,
"rack.multiprocess"=>false,
"rack.run_once"=>false,
"SCRIPT_NAME"=>"",
"QUERY_STRING"=>"q=qwerty",
"SERVER_PROTOCOL"=>"HTTP/1.1",
"SERVER_SOFTWARE"=>"puma 4.3.3 Mysterious Traveller",
"GATEWAY_INTERFACE"=>"CGI/1.2",
"REQUEST_METHOD"=>"POST",
"REQUEST_PATH"=>"/foo/bar",
"REQUEST_URI"=>"/foo/bar?q=qwerty",
"HTTP_VERSION"=>"HTTP/1.1",
"HTTP_HOST"=>"0.0.0.0:5000",
"HTTP_USER_AGENT"=>"curl/7.54.0",
"HTTP_ACCEPT"=>"*/*",
"CONTENT_LENGTH"=>19,
"CONTENT_TYPE"=>"application/x-www-form-urlencoded",
"SERVER_NAME"=>"0.0.0.0",
"SERVER_PORT"=>5000,
"PATH_INFO"=>"/foo/bar",
"REMOTE_ADDR"=>"127.0.0.1",
"rack.hijack?"=>true,
"rack.hijack"=>
"#<Proc:0x00007fc0e2166c78 .../ruby/gems/2.7.0/gems/rack-2.2.2/lib/rack/lint.rb:567>",
"rack.input"=>"#<Rack::Lint::InputWrapper:0x00007fc0e2166ae8>",
"rack.url_scheme"=>"http",
"rack.after_reply"=>[],
"rack.tempfiles"=>[]
}
Здесь смешаны несколько групп параметров:
- мета-переменные
- заголовки
- служебные параметры
Мета-переменные запроса - это версия HTTP протокола, метод (POST, GET итд), путь и query-параметры из URL:
"HTTP_VERSION"=>"HTTP/1.1"
"SCRIPT_NAME"=>""
"QUERY_STRING"=>"q=qwerty"
"SERVER_PROTOCOL"=>"HTTP/1.1"
"SERVER_SOFTWARE"=>"puma 4.3.3 Mysterious Traveller"
"GATEWAY_INTERFACE"=>"CGI/1.2"
"REQUEST_METHOD"=>"POST"
"REQUEST_PATH"=>"/foo/bar"
"REQUEST_URI"=>"/foo/bar?q=qwerty"
"SERVER_NAME"=>"0.0.0.0"
"SERVER_PORT"=>9292
"PATH_INFO"=>"/foo/bar"
"REMOTE_ADDR"=>"127.0.0.1"
"CONTENT_LENGTH”=>19
"CONTENT_TYPE"=>"application/x-www-form-urlencoded"
Еще передаются HTTP-заголовки с характерной приставкой HTTP_
в имени.
Хотя мы и не указывали заголовки, curl
добавил их сам:
"HTTP_HOST"=>"0.0.0.0:9292"
"HTTP_USER_AGENT"=>"curl/7.54.0"
"HTTP_ACCEPT"=>"*/*"
Может казаться странным, что CONTENT_LENGTH
и CONTENT_TYPE
передаются как мета-переменные, а не заголовки с приставкой HTTP_
.
Rack здесь следует стандарту CGI, который описан в RFC
3875.
Есть еще одна группа служебных параметров с приставкой rack.
:
"rack.version"=>[1, 3]
"rack.errors"=>"#<Rack::Lint::ErrorWrapper:0x00007fc0e2166a98>"
"rack.multithread"=>true
"rack.multiprocess"=>false
"rack.run_once"=>false
"rack.hijack?"=>true
"rack.hijack"=>"#<Proc:0x00007fc0e2166c78 .../ruby/gems/2.7.0/gems/rack-2.2.2/lib/rack/lint.rb:567>"
"rack.input"=>"#<Rack::Lint::InputWrapper:0x00007fc0e2166ae8>"
"rack.url_scheme"=>"http"
"rack.after_reply"=>[]
К примеру, rack.input
- это тело запроса. А параметры rack.hijack?
и
rack.hijack
связаны с очень необычной фичей socket hijacking.
Приложение может работать с сетевым сокетом напрямую и как читать так и
писать в него. Таким образом, оно может использовать другой отличный от
HTTP протокол, например websocket‘ы. Детальнее об этом написано
здесь.
Интерфейсы в спецификации
Наверняка вы заметили один нюанс. Тело запроса это не строка и не
массив, а какой-то странный объект Rack::Lint::InputWrapper
. В самом
деле, Rack-спецификация не требует использовать конкретные классы
(Hash
или, например, Array
). Спецификация описывает интерфейс
объектов, т.е. какие методы они должны поддерживать. Например, тело
запроса должно быть IO
-like объектом и реализовывать методы gets
,
each
, read
and rewind
. Это может быть, к примеру, StringIO
или
File
или любой произвольный класс, удовлетворяющий этому интерфейсу.
Спецификация требует, чтобы:
- статус ответа поддерживал метод
to_i
, который возвращает числовое значение (например, 200 или 404) - заголовки в ответе не обязательно возвращать в виде
Hash
‘а. Этот объект должен реализовать только методeach
и итерировать по парам ключ-значение - тело ответа тоже не обязано быть именно массивом, объектом класса
Array
. Этот объект должен поддерживать методeach
и итерировать по строкам - частям тела ответа. Опционально этот объект может иметь и другие методы -to_path
иclose
.
Gem rack
Перейдем теперь к самому gem‘у rack
. Один из его авторов, Leah
Neukirchen, в статье Introducing
Rack
пишет, что Rack это с одной стороны спецификация, а с другой - решения
стандартных задач веб-приложения и способ их комбинации.
Если заглянуть в исходники gem‘а, то можно выделить несколько категорий файлов, которые, к сожалению, не подчеркнуты структурой директорий:
- middlewares
- инструменты для веб-серверов и фреймворков
- инструменты для разработки новых middlewares
Middlewares
Rack предлагает подход, когда приложение использует уже готовые компоненты для обработки запроса. Эти компоненты (фильтры или middleware) образуют цепочку и по очереди обрабатывают входящий запрос перед тем как передать его приложению. Middleware имеют стандартный интерфейс. Если вынести какую-то логику приложения в отдельное middleware, то его можно повторно использовать в другом приложении на другим фреймворке и запускать на другом веб-сервере.
Рассмотрим простое middleware. Оно ничего не делает и просто вызывает
следующий middleware (app
):
class SimpleMiddleware
def initialize(app)
@app = app
end
def call(env)
# analize request
status, headers, body = @app.call(env)
# analize response
[status, headers, body]
end
end
В gem rack
входит большая коллекция уже готовых middleware.
Приведем несколько примеров:
- Rack::CommonLogger - логирует все входящие запросы в формате веб-сервера Apache
- ConditionalGet - возвращает ответ с 304 HTTP статусом без тела, если у браузера уже
закешированна актуальная версия документа, после проверки заголовков
If-None-Match
иIf-Modified-Since
- ContentLength - выставляет заголовок ContentLength если приложение это не сделало
- Directory - файловый браузер для заданной директории на сервере - можно просматривать содержимое директорий и файлов
- ETag - вычисляет хеш-сумму используя SHA256 и добавляет заголовок
ETag
- Lock - предотвращает параллельное выполнение запросов - теперь они выполняются по очереди один за одним. Это полезно если запускать потоко-небезопасное приложение на многопоточном веб-сервере, таком как Puma.
Этот подход с цепочкой middleware используют не только в приложениях
но также и во фреймворках. Например, в Rails реализовали целую
пачку
middleware. Rails также использует несколько middleware из состава
rack
:
- Rack::Sendfile
- Rack::Cache
- Rack::Lock
- Rack::Runtime
- Rack::MethodOverride
- Rack::Head
- Rack::ConditionalGet
- Rack::ETag
- Rack::TempfileReaper
Кроме коллекции middlewares в Rack и Rails есть множество готовых middleware в виде отдельных gem‘ов. Перечислим несколько самых популярных:
warden
(аутентификация, используется в Devise)rack-timeout
rack-attack
rack-reverse-proxy
rack-cors
Интеграция с серверами и фреймворками
Перейдем к следующей группе - инструменты для разработчиков веб-серверов и фреймворков.
Rack::Server
https://github.com/rack/rack/blob/2-2-stable/lib/rack/server.rb
Вы не задумывались, как команда rackup
запускает приложение? Ведь
должен запускаться полноценный веб-сервер. Конечно же, есть Webrick
из
стандартной библиотеки Ruby. Но он не поддерживает Rack-спецификацию.
Для этого в составе rack
есть класс Rack::Server
, который и
запускает приложение на, к примеру, сервере Webrick
. Под капотом
команда rackup
как раз и использует Rack::Server
(source):
#!/usr/bin/env ruby
# frozen_string_literal: true
require "rack"
Rack::Server.start
Сервер анализирует параметры командной строки, которые передали в
rackup
. Он поддерживает много опций, например --daemonize
для
запуска сервера в фоновом режиме. Можно указать порт (--port
) и хост
(--host
), на которых запустить веб-сервер. Можно даже указать какой
именно веб-сервер использовать опцией --server
.
Сервер можно запустить и программно. По-умолчанию он пытается загрузить
приложение из файла config.ru
(путь к файлу и имя можно задать опцией
:config
). Можно указать приложение явно передав опцию :app
(пример
из документации):
Rack::Server.start(
:app => lambda do |e|
[200, {'Content-Type' => 'text/html'}, ['hello world']]
end
)
Сервер также можно использовать и для отладки middleware (например
Rack::ShowExceptions
) используя приложение-заглушку:
require 'rack'
app = lambda do |e|
[200, {'Content-Type' => 'text/html'}, ['hello world']]
end
Rack::Server.start(
app: Rack::ShowExceptions.new(app), Port: 9292
)
Занятно, что команда Rails rails server
под капотом тоже использует
Rack::Server
(source).
Rask::Handler
https://github.com/rack/rack/blob/2-2-stable/lib/rack/handler.rb
Мы уже знаем, что Rack::Server
может запускать разные веб-серверы. Для
этого в Rack есть адаптеры (или иначе - handler‘ы) для поддерживаемых
веб-серверов. Адаптеры программно настраивают и запускают веб-сервер
передавая ему конфигурацию и само приложение.
Из коробки доступны следующие адаптеры:
Так же есть адаптеры к другим веб-серверам в отдельных gem‘ах. Например, для:
- Puma
- PhusionPassenger
- Falcon
- iodine
- Unicorn
- и других Unicorn-like серверов.
Если сервер явно указан, то Rack::Server
пытается загрузить его
адаптер из следующего списка - Puma, Thin, Falcon и Webrick. Обратите
внимание, что в этом списке нет Unicorn’а.
Rack::Server.start(app: app, server: :puma)
Запустить веб-сервер программно с определенным адаптером можно и
напрямую без Rack::Server
:
require 'rack'
app = lambda do |e|
[200, {'Content-Type' => 'text/html'}, ['hello world']]
end
Rack::Handler::Thin.run app
Rack::Builder
https://github.com/rack/rack/blob/2-2-stable/lib/rack/builder.rb
Наверняка, глядя на примеры файла config.ru
, вы задавались вопросом, а
что это за метод run app
:
app = lambda do |env|
[200, { 'Content-Type' => 'text/plain' }, ['Hello']]
end
run app
Очевидно, что это не стандартный метод из класса Object
, раз он
запускает Rack-приложение. На самом деле метод run
- это метод класса
Rack::Builder
, а код из файла config.ru
выполняется в контексте
объекта этого класса. Rack::Builder
используют для сборки
Rack-приложения из составляющих - приложения и middleware.
В нем доступны следующие методы:
#use
Добавляет еще одно middleware в цепочку. Теперь любой HTTP запрос сначала будет обработан этим middleware и только потом дойдет до приложения.
use Rack::ShowExceptions
run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
#run
Задает конечное приложение
run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
#map
Это такой себе наколенный роутинг. Можно задать path и Rack-приложение, которое будет обрабатывать все запросы по этому path‘у.
Rack::Builder.app do
map '/heartbeat' do
run Heartbeat
end
run App
end
Для вложенного приложения можно настроить свой независимый стек
middleware используя use
.
#warmup
Разогревает приложение перед началом обработки запросов.
warmup do |app|
client = Rack::MockRequest.new(app)
client.get('/')
end
#freeze_app
Замораживает (вызывает метод freeze
) приложение и все
входящие в него middleware.
freeze_app
Никто не запрещает использовать Rack::Builder
напрямую в config.ru
,
например вот так:
app = Rack::Builder.app do
use Rack::CommonLogger
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
end
run app
Думаю, что Rack::Builder
удобен разве что для маленьких
Rack-приложений без использования какого-либо фреймворка. Уже для Rails
его не хватает и там используют свой навороченный билдер
ActionDispatch::MiddlewareStack
(source).
Давайте посмотрим, как загружается файл config.ru
. До текущей версии
Rack v2.2 использовали следующую примитивную реализацию
(source):
def self.new_from_string(builder_script, file = "(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
Сейчас уже используют более хитрый способ (source):
# Evaluate the given +builder_script+ string in the context of
# a Rack::Builder block, returning a Rack application.
def self.new_from_string(builder_script, file = "(rackup)")
# We want to build a variant of TOPLEVEL_BINDING with self as
# a Rack::Builder instance.
# We cannot use instance_eval(String) as that would resolve constants
# differently.
binding, builder = TOPLEVEL_BINDING.eval(
'Rack::Builder.new.instance_eval { [binding, self] }'
)
eval builder_script, binding, file
builder.to_app
end
Интересно, как же Rack::Builder
используют веб-серверы. Он нужен как
минимум, чтобы загрузить файл config.ru
.
Посмотрим на Unicorn. Он файл загружают самостоятельно и делает
вызов Rack::Builder.new
(source):
eval("Rack::Builder.new {(\n#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
В Puma наоборот используют вспомогательный метод Rack::Builder.parse_file
,
который появился в rack
уже довольно давно
(source):
rack_app, rack_options = rack_builder.parse_file(rackup)
Непонятно зачем, но в Puma есть своя обрезанная копия Rack::Builder
(source).
Ее используют, если не получается подключить файл с Rack::Builder
(source).
Разработка middleware
В состав gem‘а входят также вспомогательные классы, которые упрощают разработку и тестирование новых middleware. Давайте их кратко разберем.
Rack::Request
https://github.com/rack/rack/blob/2-2-stable/lib/rack/request.rb
Это всем нам знакомый по Rails объект request
. Rack::Request
- это
тонкая обертка над исходным env
и позволяет работать ним как с
объектом используя многочисленные getter‘ы. Также у него есть
несколько удобных предикатов.
Наприме, следующий HTTP-запрос
curl http://0.0.0.0:5000/foo/bar\?q\=qwerty -d 'Hi'
превращается в такой request
:
request = Rack::Request.new(env)
request.body # => #<Rack::Lint::InputWrapper:0x00007ffa193313d0>
request.body.read # => Hi
request.path # => /foo/bar
request.request_method # => POST
request.query_string # => q=qwerty
request.content_length # => 2
request.user_agent # => curl/7.54.0
request.scheme # => http
request.host # => 0.0.0.0
request.post? # => true
request.get? # => false
request.GET # => {"q"=>"qwerty"}
request.POST # => {"Hi"=>nil}
request.params # => {"q"=>"qwerty", "Hi"=>nil}
Из полезного он еще умеет парсить:
- Query строку,
- POST-параметры
- Multipart тело запроса
- Cookies
- а также заголовки
Accept-Encoding
иAccept-Language
.
Rack::Request
учитывает X_FORWARDED_
заголовки и используют довольно
сложную логику, чтобы получить IP адрес HTTP клиента.
Rack::Response
https://github.com/rack/rack/blob/2-2-stable/lib/rack/response.rb
Rack::Response
помогает сформировать HTTP-ответ - можно удобно задать
заголовки и тело ответа. Также есть setter‘ы для некоторых заголовков.
Приведем пример:
response = Rack::Response.new(['Hello'], 200, {})
response.content_type = 'text/plain'
response.etag = 'v58.1.0'
response.set_header('Expires', 'Wed, 21 Oct 2015 07:28:00 GMT')
response.set_header('Age', '24')
response.set_cookie('id', '56817838490203423')
response.finish # => [status, headers, body]
В результате выходит вот такой HTTP ответ:
HTTP/1.1 200 OK
Content-Type: text/plain
ETag: v58.1.0
Expires: Wed, 21 Oct 2015 07:28:00 GMT
Age: 24
Set-Cookie: id=56817838490203423
Content-Length: 5
Hello
Еще можно сформировать ответ с редиректом:
response = Rack::Response.new
response.redirect('https://wikipedia.org/wiki/CGI')
response.finish # => [status, headers, body]
Он выставляет статус 302 по-умолчанию и заголовок Location
:
HTTP/1.1 302 Found
Location: https://wikipedia.org/wiki/CGI
Content-Length: 0
Rack из коробки поддерживает стриминг данных, когда очередной фрагмент
вычисляется на лету, а общая длина ответа заранее не известна. В
Rack::Response
для этого есть метод finish
, который принимать
опционально блок, и используя метод write
можно досылать клиенту
очередной фрагмент ответа.
В примере ниже приложение отдает ответ с заголовком Transfer-Encoding:
chunked
- он выставляется автоматически если не задали тело ответа до
вызова finish
. Чтобы стриминг действительно заработал и не
буферизировался сервером, сервер должен поддерживать стриминг. Puma как
раз один из таких серверов.
Здесь мы шлем несколько строчек в специальном chunked-формате с задержкой в 1 секунду перед отправкой каждой строки:
response = Rack::Response.new
response.finish do |r|
(1..10).each do |i|
message = "line ##{i}"
size = message.size.to_s(16)
r.write("#{size}\r\n")
r.write("#{message}\r\n")
sleep 1
end
r.write("0\r\n")
r.write("\r\n")
end
Запустим Puma вот такой командой (с rackup
по какой-то причине
стриминг не заработал):
$ puma --port 5000 config.ru
Чтобы curl
не буферизировал ответ и выводил каждый фрагмент сразу при
получении надо указать опцию --no-buffer
:
$ curl --no-buffer http://0.0.0.0:5000
7
line #1
7
line #2
7
line #3
7
line #4
7
line #5
7
line #6
7
line #7
7
line #8
7
line #9
8
line #10
0
Каждая строка выводится с интервалом в 1 секунду.
Rack::BodyProxy
https://github.com/rack/rack/blob/2-2-stable/lib/rack/body_proxy.rb
Это простой декоратор для тела ответа. Он позволяет вызвать callback в конце после отправки всего тела ответа.
Так, например, можно освободить блокировку в Rack::Lock
(source):
returned = response << BodyProxy.new(response.pop) { unlock }
или прологировать запрос в Rack::CommonLogger
(source):
body = BodyProxy.new(body) { log(env, status, headers, began_at) }
Обычно его используют, чтобы просто закрыть оригинальный body если его подменяют другим, как требует спецификация:
body = Rack::BodyProxy.new(new_body) do
original_body.close if original_body.respond_to?(:close)
end
Rack::Utils::HeaderHash
https://github.com/rack/rack/blob/v2.2.2/lib/rack/utils.rb#L413-L497
Rack::Utils::HeaderHash
это case-insensitive Hash
. Несмотря на то,
что он помечен как приватное api, он весьма полезен при разработке
middleware.
По стандарту имя HTTP-заголовка может быть в любом регистре. И это
совершенно корректно, если приложение вернет вместо Content-Type
что-нибудь вроде content-type
или CONTENT-TYPE
. Об этом легко забыть
и ожидать в ответе заголовки в традиционном camel case формате.
Rack::Utils::HeaderHash
как раз и решает эту проблему. Если обернуть
заголовки ответа в этот класс, то можно забыть о проблеме с регистром.
headers = Rack::Utils::HeaderHash.new(
"content-type" => "application.json", "Etag" => "v1"
)
headers # => {"content-type"=>"application.json", "Etag"=>"v1"}
headers["content-type"] # => "application.json"
headers["CONTENT-TYPE"] # => "application.json"
headers["content-TYPE"] = "application/xml"
headers["content-type"] # => "application/xml"
Более того, по спецификации объект с заголовками ответа должен
реализовывать только метод each
и, следовательно, с ним нельзя
работать как с обычным Hash
‘ом. Здесь опять приходит на помощь
Rack::Utils::HeaderHash
- он реализует несколько методов обычного
Hash
‘а - []
, []=
, key?
, has_key?
, include?
и др.
Rack::Utils::HeaderHash
поддерживает еще одну возможность. Если
приложению нужно вернуть несколько значений одного и того же
заголовка (например, для Set-Cookies
) то можно вернуть одну строку,
которая содержит все значения заголовка разделенные символом новой
строки “\n”. А уже веб-сервер сам это разделит обратно и сформирует
корректный ответ. Rack::Utils::HeaderHash
помогает задать
такое множественное значение заголовка в виде массива, который в итоге
конвертируется в строку с символом разделителем “\n”:
h = Rack::Utils::HeaderHash[{}]
h['Set-Cookes'] = ['a', 'b']
h.to_hash # => {"Set-Cookes"=>"a\nb"}
Rack::Lint
https://github.com/rack/rack/blob/2-2-stable/lib/rack/lint.rb
Это обычный middleware, который проверяет корректность приложения или middleware и соответствие Rack-спецификации. Используется в юнит-тестах на middleware.
Rack::Lint
проверяет запрос (объект env
) по следующим критериям:
- Это объект класса
Hash
- Не frozen
- Должен содержать все мета-переменные с валидными значениями
rack.input
(тело запроса) должен:- реализовывать все обязательные методы (
gets
,each
,read
,rewind
) gets
/read
должны возвращать строкиeach
должен итерировать по строкам
- реализовывать все обязательные методы (
rack.error
должен реализовывать методыputs
,write
,flush
Rack::Lint
проверяет ответ по следующим критериям:
- Это
Array
из трех элементов - Статус конвертируется в число методом
to_i
и находится в допустимом диапазоне значений (> 100) - Заголовки:
- Объект с заголовками поддерживает метод
each
- Все ключи - строки
- Имена заголовков не содержат префикс
rack.
- Имена заголовков должны соответствовать RFC7230, состоять только из печатный символов и не содержать запрещенные спецсимволы ((),/:;<=>?@[]{})
- Значения - строками
- В ответе нет заголовков
Content-Type
иContent-Length
если статус ответа 1xx, 204 или 304
- Объект с заголовками поддерживает метод
- Тело ответа:
- Реализует метод
each
- Состоит из коллекции строк
- Общая длина в байтах соответствует заголовку
Content-Length
- Если реализован метод
to_path
, то этот файл существует
- Реализует метод
Rack::MockRequest
https://github.com/rack/rack/blob/2-2-stable/lib/rack/mock.rb#L22
Используется в юнит-тестах middleware.
Предполагается тестировать middleware двумя способами. В одном из них
используя метод env_for
можно сгенерировать запрос (env
-объект) со
всеми обязательными параметрами и заголовками. Далее можно вызвать метод
call
на тестируемом middleware и проверить ответ и изменения в
env
:
env = Rack::MockRequest.env_for("/?_method=delete", method: "GET")
status, headers, body = app.call env
env["REQUEST_METHOD"].must_equal "GET"
Во втором подходе Rack::MockRequest
оборачивает собой middleware
(app
) и имитирует HTTP-запросы. В результате возвращается объект
класса Rack:MockResponse
. Тот в свою очередь содержит в себе ответ
middleware и позволяет работать с ним через удобные getter‘ы:
res = Rack::MockRequest.new(app).get("/foo")
res.must_be :ok?
res["X-ScriptName"].must_equal "/foo"
res["X-PathInfo"].must_equal ""
res.body.must_equal ""
Утилиты
В gem‘е есть также вспомогательные классы:
Rack::Mime
- определяет MIME type (например, “image/png”) по расширению файлаRack::MediaType
- парситContent-Type
заголовок и возвращает MIME type и параметры. Например для “text/plain;charset=utf-8” вернется MIME type “text/plain” и параметры{'charset' => 'utf-8'}
Rack::Multipart
- парсит multipart запросRack::QueryParser
- парсит query параметры из URL; поддерживает вложенные параметры, например"foo[]=1&foo[]=2"
Rack::RewindableInput
- обертка надrack.input
объектом;rack.input
обязан поддерживать методrewind
, перемотку в самое начало тела запроса;Rack::RewindableInput
реализуетrewind
буферизируя данные и, таким образом, можно превратить не-rewindable объект в корректный rewindable.Rack::Utils
- коллекция вспомогательных методов.
Демо
И напоследок. В состав gem‘а входит маленькая демка - Rack-приложение
Rack::Lobster
(source),
которое выводит изображение лобстера (морского рака - видимо это такой
каламбур) в ASCII-арте. Если нажать на ссылку “flip”, то лобстер
повернется в другую сторону.
Чтобы запустить демку надо клонировать git-репозиторий и выполнить команду:
$ rackup example/lobster.ru
В результате получим вот такую страницу:
Резюме
Давайте подытожим, зачем же нужен gem rack
. Во-первых, это большая
коллекция Rack middleware. Во-вторых, он нужен для загрузки файла
config.ru
, стартовой точки Rack-приложения. И в-третьих, в нем
есть инструменты для разработки и тестирования новых middlewares.
Ссылки
- https://github.com/rack/rack/blob/2-2-stable/SPEC.rdoc
- https://tools.ietf.org/html/rfc3875
- http://leahneukirchen.org/blog/archive/2007/02/introducing-rack.html
- https://guides.rubyonrails.org/rails_on_rack.html
- https://blog.sqreen.com/fixing-a-critical-issue-a-journey-into-ruby-web-server-startup-sequences-part-two/