Skip to content

Запросы и ответы HTTP ​

HTTP-сервер преобразует соединение клиента в запрос PHP, а ответ PHP - в сетевые данные. Он использует библиотеку hyper и принимает HTTP/1.1 и HTTP/1.0. Он не перенаправляет запросы другому серверу.

Middleware может ответить на запрос до запуска PHP. Rapira использует middleware, чтобы отдавать статические файлы.

HTTP-сервер принимает незашифрованный HTTP. Используйте прокси для завершения TLS. См. раздел Запуск в продакшене.

Проверка запроса ​

HTTP-сервер проверяет каждый запрос до запуска PHP. Если запрос не проходит проверку, сервер не вызывает PHP.

Rapira возвращает 501 для запроса CONNECT. HTTP-сервер не создаёт туннели.

Rapira принимает абсолютную форму цели запроса, например GET http://host.example/admin?x=1 HTTP/1.1. Rapira удаляет данные пользователя из authority цели. Затем authority заменяет поле Host, поэтому $_SERVER['HTTP_HOST'] и цель совпадают. PHP получает путь и строку запроса в форме origin в $_SERVER['REQUEST_URI'].

http.keepalive_timeout_secs ограничивает каждое чтение от клиента. Ограничение действует на неактивное соединение и заголовки запроса. Rapira возвращает 408, если не получает данные тела запроса до истечения времени. Затем Rapira закрывает соединение. Значение по умолчанию - 60 секунд.

toml
[http]
keepalive_timeout_secs = 60

От имени заголовка к ключу $_SERVER ​

CGI переводит имя поля запроса в верхний регистр, заменяет каждый - на _ и добавляет HTTP_. См. RFC 3875 §4.1.18. Поэтому X-Forwarded-For становится HTTP_X_FORWARDED_FOR.

При регистрации переменной PHP выполняет ещё одно преобразование. Он также заменяет . на _. Поэтому три сетевых имени поля соответствуют одному ключу PHP:

В запросеВ PHP
X-Forwarded-For$_SERVER['HTTP_X_FORWARDED_FOR']
X_Forwarded_For$_SERVER['HTTP_X_FORWARDED_FOR']
X.Forwarded.For$_SERVER['HTTP_X_FORWARDED_FOR']

Без обязательной проверки имён полей Rapira такое совпадение может создать угрозу безопасности. Прокси может задать X-Forwarded-For, а клиент - отправить X_Forwarded_For. Оба имени соответствуют одному ключу $_SERVER. Фильтр прокси для имени с дефисами может не удалить имя с подчёркиваниями. В результате приложение может доверять значению от клиента.

Rapira также задаёт переменные запроса CGI. SERVER_NAME берётся из http.server_name, значение по умолчанию - localhost. SERVER_PORT берётся из http.server_port. Значение по умолчанию - TCP-порт из http.listen или 80 для Unix-сокета. Для клиента на Unix-сокете REMOTE_ADDR равен 127.0.0.1, а REMOTE_PORT равен 0. Rapira не задаёт PATH_INFO.

Rapira принимает только незашифрованный HTTP. Поэтому $_SERVER['HTTPS'] всегда пуст, а REQUEST_SCHEME всегда равен http. За TLS-прокси настройте приложение на чтение полей прокси.

Имена, конфликтующие с CGI-переменной ​

Rapira принимает в имени поля запроса только байты A-Z, a-z, 0-9 и -. Это правило отклоняет _ и ., а также другие символы, например ~. Параметр http.unsafe_field_names задаёт действие для отклонённого имени:

  • drop - значение по умолчанию. В режимах Classic и Worker Rapira удаляет такие поля до того, как PHP их получит. Rapira пишет одну запись warn для каждого запроса. В режиме Dispatcher drop сохраняет все имена, потому что в этом режиме Rapira не помещает поля запроса в $_SERVER.
  • reject - Rapira возвращает 400 во всех режимах.
toml
[http]
unsafe_field_names = "drop"

Проверку нельзя выключить. Также нельзя добавить исключения для отдельных имён. Полный список настроек см. в разделе Конфигурация.

Замените подчёркивания в обязательном имени поля на дефисы. Rapira применяет это правило и к полям от прокси. Rapira не может определить, кто отправил поле с подчёркиванием: клиент или доверенный прокси. Настройте прокси, чтобы он изменял имя перед отправкой поля.

drop пишет свои записи с уровнем warn, но уровень лога по умолчанию - error. Установите для цели http уровень warn, чтобы видеть эти записи. См. раздел Логирование.

Поля, присланные несколько раз ​

HTTP разрешает повторяющиеся поля, но CGI предоставляет одно значение для каждой переменной. Rapira объединяет повторяющиеся значения по синтаксису поля:

  • Списочные поля: Rapira соединяет значения запятой и пробелом. Например, две строки Accept становятся text/*, image/*. RFC 9110 §5.3 разрешает этот формат для полей со значениями через запятую.
  • Cookie: Rapira соединяет значения точкой с запятой и пробелом. Такой формат ожидает парсер cookie PHP.
  • Поля с одним значением: Rapira сохраняет первую строку Authorization, Proxy-Authorization, Content-Type, Referer или From. Остальные строки Rapira игнорирует, потому что объединённое значение имеет другой смысл.
  • Host: Rapira возвращает 400 для нескольких строк Host. Это требует RFC 9112 §3.2.

До этой обработки Rapira возвращает 400 для строк Content-Length с разными значениями.

PHP получает значения полей как неизменённые байты. Поэтому cookie в Latin-1 или подписанное поле сохраняет каждый байт, который отправил клиент.

Тела запросов ​

Rapira читает тело запроса в память до запуска PHP. http.max_body_size_mb ограничивает память для одного тела. Значение по умолчанию - 8 МиБ, как у post_max_size в PHP по умолчанию. Rapira возвращает 413 для большего тела и закрывает соединение. Rapira не читает оставшиеся данные тела.

Rapira проверяет ограничение дважды:

  • Сначала Rapira проверяет объявленный Content-Length до чтения данных тела.
  • Затем Rapira проверяет ограничение при получении каждого фрагмента тела. Эта проверка ограничивает chunked-запросы без объявленной длины.

Rapira поддерживает Expect: 100-continue для запросов HTTP/1.1. Rapira отправляет 100 Continue до того, как клиент отправит тело. Rapira сначала проверяет Content-Length. Поэтому Rapira может вернуть 413 до того, как клиент загрузит слишком большое тело. Для HTTP/1.0 Rapira игнорирует ожидание, как требует RFC 9110 §10.1.1.

toml
[http]
max_body_size_mb = 8

Передача ответа ​

Режим определяет, когда HTTP-сервер получает ответ от PHP:

  • В режимах Classic и Worker Rapira хранит полный ответ в памяти. Rapira отправляет ответ в конце запроса или когда скрипт вызывает rapira_finish_request().
  • В режиме Dispatcher Rapira отправляет заголовки вместе с первой записью тела или при вызове Exchange::flush(). Затем Rapira отправляет каждый фрагмент тела, когда скрипт его записывает.

http.write_timeout_secs ограничивает время, в течение которого одна операция записи клиенту может оставаться без продвижения. Когда время истекает, Rapira закрывает соединение. Значение по умолчанию - 30 секунд.

Сервер задаёт границы ответа. Поэтому неверная длина от PHP не изменяет границы сообщений. Сервер удаляет эти поля, которые задал PHP: Content-Length, Transfer-Encoding, Connection, Keep-Alive, Upgrade, Trailer, TE и Proxy-Connection. RFC 9110 §7.6.1 определяет эти поля конкретного соединения.

Если PHP отправляет Connection, Rapira также удаляет каждое поле, которое в нём указано. После этого шага Rapira добавляет собственный Content-Length. Поэтому Connection: content-length не может удалить границы ответа.

Затем Rapira задаёт длину:

  • В режимах Classic и Worker Rapira задаёт Content-Length равным длине полного тела.
  • В режиме Dispatcher Rapira использует Content-Length, который скрипт объявляет в заголовках. Если тело короче, Rapira закрывает соединение, чтобы клиент не прочитал следующий ответ как часть текущего. Более длинное тело Rapira обрезает.
  • В режиме Dispatcher Rapira вычисляет Content-Length, когда первый writeBody() или sendFile() завершает ответ до отправки заголовка. Объявленная длина имеет приоритет. Ответ только с трейлерами получает нулевую длину.
  • Если в заголовке нет объявленной или вычисленной длины, Rapira использует chunked transfer coding для HTTP/1.1. Для HTTP/1.0 Rapira закрывает соединение после тела. Это относится к потоковой записи и раннему вызову Exchange::flush().

В ответах PHP Rapira удаляет Content-Length и не отправляет тело для 204, 304 и HEAD. Ответы HEAD для статических файлов сохраняют длину файла. См. Статические файлы.

Другие поля PHP Rapira отправляет без изменений, например повторяющиеся поля Set-Cookie, Vary и Link. В режимах Classic и Worker Rapira удаляет недопустимое поле и пишет запись лога debug, но отправляет остальную часть ответа. Rapira не отправляет промежуточные (1xx) заголовки и трейлеры от PHP. Поэтому 103 Early Hints не доходит до клиента.

Если воркер останавливается до конца тела, сервер закрывает соединение без полного завершающего маркера. Фатальная ошибка после начала вывода также может прервать ответ. В режиме Worker непойманное исключение обработчика после начала вывода прерывает ответ, но цикл продолжается. Клиент может обнаружить каждое неполное сообщение.

Отправляет ли flush() вывод раньше в режимах Classic и Worker?

Нет. Функция PHP flush() не отправляет данные клиенту. Для потоковой передачи ответа используйте режим Dispatcher. Если буферизованное тело превышает 1 ГиБ, Rapira останавливает запрос, и клиент получает неполный ответ.

Ответы об ошибках ​

HTTP-сервер отправляет ответ об ошибке, когда запрос не доходит до PHP или PHP не отправляет заголовки ответа. Такой ответ не имеет тела. Он содержит cache-control: private, no-store и connection: close.

СтатусПричина
400Запрос содержит больше одного поля Host. Запрос HTTP/1.1 не содержит поле Host или содержит пустое поле Host. Имя поля небезопасно, и задано unsafe_field_names = "reject". Ошибка чтения тела. В режиме Dispatcher тело multipart недопустимо.
408Данные тела не пришли за время http.keepalive_timeout_secs.
413Тело больше http.max_body_size_mb, или тело multipart превышает ограничение [http.uploads].
500Пул воркеров остановлен. В режиме Dispatcher Rapira не может записать загруженный файл в [http.uploads].dir.
501Запрос использует CONNECT.
502Воркер PHP остановился до отправки заголовков ответа.
503Очередь воркеров оставалась полной 30 секунд.

Rapira отправляет эти статусы без двух указанных полей:

  • 503, когда воркер не может запустить свой входной скрипт. После каждого неудачного запуска воркер отвечает этим статусом на один запрос из очереди.
  • 502, когда PHP задаёт финальный статус ниже 200, например 101.
  • 500, когда скрипт Dispatcher освобождает exchange до того, как Rapira отправит заголовки ответа.

Режим Dispatcher ​

В режиме Dispatcher Rapira не помещает поля запроса в $_SERVER. Скрипт читает запрос из объекта Rapira\Http\Exchange и записывает ответ его методами. Rapira разбирает тело multipart/form-data до того, как скрипт его получит. API, загрузки файлов и sendFile() описаны в разделе Режим Dispatcher.

Досрочное завершение ответа ​

Обработчик может продолжить работу после подготовки ответа. Например, он может отправить webhook, добавить задачу в очередь или обновить кешированные данные. Клиенту не нужно ждать эту работу.

rapira_finish_request() завершает ответ в этой точке. PHP очищает буферы вывода и передаёт ответ HTTP-серверу. HTTP-сервер отправляет ответ, пока обработчик продолжает работу. Функция работает как функция php-fpm fastcgi_finish_request(). Rapira не предоставляет fastcgi_finish_request(). Замените каждый её вызов на rapira_finish_request():

php
<?php

header('Content-Type: text/plain');
echo "Order accepted\n";

rapira_finish_request();

// Этот код выполняется после того, как клиент получил ответ.
$mailer->sendConfirmation($order);
$metrics->flush();

Сигнатура - rapira_finish_request(): bool. Файл crates/sapi/rapira.stub.php объявляет её и другие функции и классы PHP. Настройте IDE на этот файл для автодополнения и информации о типах.

rapira_finish_request() работает в режимах Classic и Worker. В режиме Dispatcher вызов выбрасывает \Error. В этом режиме завершите exchange, который возвращает receive(), и затем продолжите работу. См. разделы Режим Dispatcher и Режимы выполнения.

Функция имеет следующие ограничения:

  • Rapira удаляет вывод после вызова. Запишите весь вывод для клиента до вызова.
  • Воркер продолжает выполнять обработчик. Воркер не может принять следующий запрос, пока обработчик не вернёт управление. Поэтому вызов может уменьшить время ожидания клиента, но не добавляет параллелизм. Передавайте длительные операции в очередь. Параллелизм воркеров описан в разделе Модель процессов.