Таймауты
OxPHP применяет два независимых таймаута, которые защищают от медленных клиентов и зависших запросов. Таймаут заголовков ограничивает фазу установления соединения на уровне сервера. Время выполнения PHP ограничивается собственной ini-директивой PHP max_execution_time (и функцией времени выполнения set_time_limit()), ровно так же, как и в любом другом SAPI.
Как это работает
Каждый запрос проходит через следующие фазы:
- Соединение принято — запускается таймаут заголовков. OxPHP ждёт, пока клиент пришлёт полный набор HTTP-заголовков.
- Заголовки получены — таймаут заголовков завершается. Запрос передаётся PHP-воркеру.
- PHP обрабатывает запрос — код приложения выполняется под собственным
max_execution_timePHP (на основе SIGALRM). Когда лимит достигнут, запрос отменяется и срабатывает унифицированная фатальная ошибкаRequest cancelled (timeout). - Ответ отправлен — на keep-alive-соединениях цикл повторяется с шага 1.
graph TD A["TCP connect<br/>(+ TLS handshake if enabled)"] -->|HEADER_TIMEOUT_SECONDS| B["Headers received"] B -->|max_execution_time| C["Response sent"] C -->|next request, keep-alive| A
На keep-alive-соединениях оба таймаута применяются независимо к каждому запросу в рамках соединения.
Когда включён TLS, таймаут заголовков запускается после завершения TLS-рукопожатия, а не в момент принятия TCP-соединения.
Таймаут заголовков защищает от атак типа slowloris, когда клиент отправляет заголовки по одному байту за раз, чтобы удерживать соединения открытыми неограниченно долго.
Отсчёт времени выполнения PHP полностью делегируется PHP. Когда max_execution_time превышен, унифицированный путь отмены в OxPHP:
- Устанавливает
connection_status() & PHP_CONNECTION_TIMEOUT, чтобы код в userland мог определить причину. - Выполняет все колбэки
register_shutdown_function(), ровно так же, как это делает PHP-FPM. - Возвращает HTTP
504 Gateway Timeout, записывая сообщениеRequest cancelled (timeout)в лог ошибок.
Статус-коды отмены
OxPHP отменяет запрос по нескольким разным причинам. Каждая из них отображается в статус-код на уровне протокола, который отражает фактическое состояние, а не обобщённый 500:
| Причина | Статус | Примечания |
|---|---|---|
Превышен max_execution_time / set_time_limit() |
504 Gateway Timeout |
Исчерпание времени выполнения на стороне сервера. |
| Корректное завершение работы сервера слило запрос | 503 Service Unavailable |
Добавляет Retry-After: 5, чтобы клиенты повторили запрос к восстановленному или заменяющему экземпляру. |
| Клиент закрыл соединение в середине запроса | 499 |
«Client Closed Request» в стиле nginx. Соединение уже разорвано, поэтому этот статус появляется только в логах доступа и метриках — на провод он никогда не записывается. Отражает инициированные клиентом обрывы как не-5xx, чтобы они не засоряли алерты о серверных ошибках. |
| Воркер признан застрявшим супервизором | 500 Internal Server Error |
Обобщённая серверная ошибка — причина (взаимоблокировка, заблокированный системный вызов, …) неизвестна. |
| Отмена, инициированная в userland | 500 Internal Server Error |
Userland может установить собственный статус через http_response_code() перед запуском отмены; этот явно заданный статус сохраняется. |
Если ваш ERROR_PAGES_DIR содержит только 500.html, добавьте 504.html, 503.html и (опционально) 499.html, чтобы стилизованные страницы оставались единообразными для всех причин отмены.
Конфигурация
| Переменная | По умолчанию | Описание |
|---|---|---|
HEADER_TIMEOUT_SECONDS |
5 |
Максимальное число секунд на получение заголовков запроса после принятия соединения. Защищает от атак slowloris. 0 не обрабатывается особым образом — он передаётся в hyper как нулевой таймаут, который срабатывает немедленно. Чтобы отключить таймаут, снимите переменную, а не устанавливайте её в 0 |
Время выполнения PHP настраивается через php.ini, а не через env-переменные OxPHP:
; php.ini
max_execution_time = 30Или для отдельного скрипта во время выполнения:
set_time_limit(60); // 60 seconds from now
set_time_limit(0); // disable for this requestРекомендуемые значения
| Сценарий | Таймаут заголовков | max_execution_time |
|---|---|---|
| API-сервер | 5s | 30s |
| Общая веб-раздача | 5s | 60s |
| Загрузка файлов | 10s | 300s |
| SSE / long-polling | 5s | 0 (отключено, задавать для каждого скрипта) |
Подбирайте эти значения исходя из характеристик вашего приложения. Для SSE-эндпоинтов вызывайте set_time_limit(0) в начале потокового скрипта, а не отключайте max_execution_time глобально.
Устранение неполадок
Клиенты неожиданно получают 504 с «Request cancelled (timeout)»
Лимит времени выполнения PHP сработал раньше, чем скрипт завершился.
Решение: Повысьте max_execution_time для затронутого скрипта или вызовите set_time_limit($seconds), чтобы продлить его во время выполнения:
// at the top of a slow script
set_time_limit(300);Для SSE- или потоковых эндпоинтов, где соединения должны оставаться открытыми неограниченно долго, отключите таймер для этого скрипта:
set_time_limit(0);Соединения обрываются до прихода заголовков
Таймаут заголовков слишком короток для клиентов на каналах с высокой задержкой или за медленными балансировщиками нагрузки.
Решение: Увеличьте таймаут заголовков:
HEADER_TIMEOUT_SECONDS=15OPcache приводит к таймауту первого запроса после изменения скрипта
Перекомпиляция OPcache добавляет задержку на первом запросе после изменения файла. Это чаще встречается в средах разработки с большим числом файлов. Повысьте max_execution_time или отключите его на время разработки.
Пример для Docker
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "8080:8080"
environment:
HEADER_TIMEOUT_SECONDS: "5"
volumes:
- ./app:/var/www/html:ro
- ./php.ini:/usr/local/etc/php/conf.d/zz-app.ini:roС php.ini, содержащим:
max_execution_time = 30Рекомендуемые практики
- Никогда не устанавливайте
max_execution_time = 0глобально в продакшене, если только у вас нет SSE- или long-polling-эндпоинтов, которым требуются неограниченные по времени соединения. Предпочитайтеset_time_limit(0)для каждого скрипта отдельно. - Используйте более короткие лимиты для API-серверов. У API предсказуемое время ответа. 30-секундный
max_execution_timeбыстро отлавливает застрявшие запросы, не влияя на обычный трафик. - Сочетайте с ограничением частоты запросов. Таймауты защищают отдельные запросы; ограничение частоты запросов защищает от большого объёма запросов. Вместе они покрывают как медленные, так и быстрые шаблоны атак.
Смотрите также
- Ограничение частоты запросов — ограничение частоты запросов по IP
- Server-Sent Events (SSE) — руководство по отключению таймаутов для потоковых эндпоинтов
- Справочник по конфигурации — полный справочник переменных окружения