Статические файлы

OxPHP отдаёт статические файлы напрямую из корневого каталога документов, не запуская PHP. Каждый файл отдаётся с автоматическим определением MIME-типа, кэшированием в памяти для быстрого повторного доступа и полноценным HTTP-кэшированием: ETag, условные запросы и запросы Range для частичной загрузки.

Как это работает

Когда запрос соответствует статическому файлу:

  1. Файл найден — слой маршрутизации сопоставляет путь URL с файлом на диске
  2. Определение MIME — тип содержимого определяется по расширению файла
  3. Проверка кэша — кэш файлов проверяется перед обращением к файловой системе
  4. Условная проверка — если запрос содержит If-None-Match или If-Modified-Since, OxPHP вычисляет условие и может вернуть 304 Not Modified, не отправляя тело ответа
  5. Проверка Range — если запрос GET или HEAD содержит заголовок Range, OxPHP отвечает 206 Partial Content: GET получает только запрошенный диапазон байтов, HEAD — те же заголовки диапазона без тела
  6. Ответ — файлы размером до 1 MiB отдаются из кэша в памяти; файлы большего размера передаются потоком напрямую с диска, если только их не предстоит сжать

Конфигурация

Переменная Значение по умолчанию Описание
STATIC_MAX_AGE 30d Cache-Control: max-age для статических файлов. Принимает 30s, 5m, 2h, 30d, 1w, 1y, просто число секунд (например, 3600) или off, чтобы полностью отключить заголовки кэширования. Заменяет устаревшую STATIC_CACHE_TTL.
STATIC_REVALIDATE off Установите on, чтобы включить ревалидацию по mtime для кэша содержимого в памяти (перепроверяет каждый файл не чаще одного раза в 3 секунды; изменения становятся видимыми в пределах этого окна). Заменяет устаревшую STATIC_CACHE (где off имело обратное значение).

Определение MIME

MIME-типы определяются автоматически по расширению файла. Если тип определить не удаётся, сервер использует запасной вариант application/octet-stream. Наиболее распространённые сопоставления:

Расширение Content-Type
.html text/html
.css text/css
.js text/javascript
.json application/json
.png image/png
.svg image/svg+xml
.woff2 font/woff2

Кэширование файлов

OxPHP использует кэш в памяти, чтобы снизить количество обращений к диску для часто запрашиваемых файлов:

  • Файлы размером до 1 MiB (1 048 576 байт) считываются в память и кэшируются. Общий бюджет кэша составляет 64 MiB (67 108 864 байта). При превышении бюджета вытесняются записи, к которым дольше всего не обращались, чтобы освободить место.
  • Файлы размером больше 1 MiB не кэшируются, а передаются потоком напрямую с диска. Заголовок Content-Length берётся из метаданных файла, поэтому клиент заранее знает полный размер.
  • Единственное исключение из потоковой передачи — сжимаемый файл, который ещё укладывается в окно сжатия (3 MiB). Для клиентов, принимающих какое-либо кодирование содержимого, он считывается целиком, потому что сервер не может закодировать тело, которого у него нет полностью. Поскольку такой файл превышает лимит кэша, он сжимается заново при каждом таком запросе. Поставьте CDN перед ресурсами этого диапазона или держите их меньше 1 MiB — тогда работа выполняется один раз. Одновременно удерживается лишь ограниченное число таких файлов; запрос, пришедший сверх этого лимита, получает файл потоком и без кодирования, а не ждёт, пока освободится место.

Кэш файлов заполняется при первом запросе к каждому файлу и сохраняется между последующими запросами. По умолчанию записи кэша хранятся до тех пор, пока не будут вытеснены политикой LRU.

Ревалидация содержимого

Установите STATIC_REVALIDATE=on, чтобы включить ревалидацию по mtime. В этом режиме сервер перепроверяет время изменения кэшированного файла системным вызовом stat() не чаще одного раза в 3 секунды на файл, а не при каждом запросе. Если файл на диске изменился, устаревшая запись вытесняется, и файл автоматически считывается заново. В пределах 3-секундного окна кэшированная запись отдаётся прямо из памяти без системного вызова, поэтому затраты амортизируются, а не оплачиваются на каждом запросе. Изменения на диске становятся видимыми в течение 3 секунд.

Разработка

Включите STATIC_REVALIDATE=on в процессе разработки, чтобы видеть изменения файлов без перезапуска сервера. Оставьте её невыставленной в продакшене (значение по умолчанию off) для максимальной пропускной способности с нулевыми накладными расходами на системные вызовы при каждом запросе.

HTTP-кэширование

Cache-Control

Когда задана STATIC_MAX_AGE (значение по умолчанию — 30d), каждый ответ со статическим файлом содержит заголовок Cache-Control:

http
Cache-Control: public, max-age=2592000

Значение max-age — это TTL, переведённый в секунды. Установите STATIC_MAX_AGE=off, чтобы полностью убрать этот заголовок.

ETag и Last-Modified

Каждый ответ со статическим файлом содержит:

  • ETag — сильный ETag в формате "<size>-<mtime_hex>", вычисляемый из размера файла и времени последнего изменения. Сильный валидатор также удовлетворяет If-Range, поэтому прерванные загрузки можно безопасно возобновлять. Когда ответ отдаётся сжатым, в любом из кодирований, тег ослабляется до W/"…": сжатые байты представляют собой другое представление, и слабый тег по-прежнему позволяет выполнять ревалидацию (304), но предотвращает смешивание сжатых и несжатых фрагментов при возобновлении.
  • Last-Modified — HTTP-дата по RFC 9110 на основе времени изменения файла

Эти заголовки позволяют браузерам и CDN проверять кэшированные копии, не загружая файл повторно.

Условные запросы (304)

OxPHP анализирует заголовки условных запросов, чтобы не отправлять неизменившееся содержимое файла:

  • If-None-Match — клиент отправляет закэшированный им ETag. Если он совпадает с текущим файлом, OxPHP возвращает 304 Not Modified без тела.
  • If-Modified-Since — клиент отправляет метку времени. Если файл не изменялся с этого момента, OxPHP возвращает 304.

If-None-Match имеет приоритет над If-Modified-Since согласно RFC 9110. Для файлов, уже находящихся в кэше в памяти, условная проверка выполняется без каких-либо обращений к диску.

Запросы Range (206)

Ответы со статическими файлами объявляют Accept-Ranges: bytes, и запросы GET с заголовком Range, содержащим один диапазон, получают только запрошенные байты:

http
GET /videos/intro.mp4 HTTP/1.1 Range: bytes=1048576- HTTP/1.1 206 Partial Content Content-Range: bytes 1048576-52428799/52428800 Content-Length: 51380224

Это обеспечивает перемотку <video>/<audio> в браузерах, возобновляемые загрузки (wget -c, менеджеры загрузок) и частичную загрузку PDF. Поддерживаются все три формы диапазона из RFC 9110: bytes=N-M, bytes=N- (от смещения до конца) и bytes=-N (последние N байт).

  • Диапазон, который невозможно удовлетворить (начало за концом файла), возвращает 416 Range Not Satisfiable с Content-Range: bytes */<size>.
  • If-Range учитывается: когда клиент отправляет ETag (или дату Last-Modified) своей частичной копии, а файл с тех пор изменился, OxPHP возвращает полный ответ 200 вместо несоответствующего фрагмента. Форма с датой принимается только после того, как секунда изменения файла полностью истекла — только что записанный файл может измениться снова в пределах той же секунды, не сдвинув дату, поэтому она пока не является сильным валидатором (RFC 9110).
  • Запросы с несколькими диапазонами (bytes=0-1,4-5) получают файл целиком как 200 OK — ответы multipart/byteranges не генерируются.
  • Запросы HEAD с заголовком Range получают те же заголовки 206/Content-Range, что и GET, но без тела — как в nginx и Apache.
  • Диапазоны и сжатие взаимоисключающи. Для клиентов, принимающих любое кодирование содержимого, обработка диапазонов отключается для представлений, которые были бы отданы в сжатом виде, а сжатые ответы не объявляют Accept-Ranges, поскольку иначе возобновлённая загрузка могла бы приклеить несжатые байты к сжатому префиксу. Файлы больше окна сжатия (3 MiB) никогда не сжимаются, поэтому диапазоны всегда работают для содержимого, которому они действительно нужны: видео, архивов и изображений. Ответы для файлов, подлежащих сжатию, всегда содержат Vary: Accept-Encoding, даже когда отдаются без сжатия, чтобы разделяемые кэши хранили варианты раздельно.
  • Ответы 206 никогда не сжимаются, а обработка диапазонов не применяется к ответам PHP — только к статическим файлам.

Пример: возобновление прерванной загрузки с помощью curl:

bash
curl -C - -O https://example.com/dist/app-installer.dmg

Отключение кэширования

Существуют два независимых слоя кэширования и по переменной для каждого:

Переменная Что контролирует Эффект off
STATIC_MAX_AGE=off Кэш браузера (HTTP-заголовки) Заголовки Cache-Control, ETag и Last-Modified не отправляются
STATIC_REVALIDATE=on Серверный кэш в памяти Перепроверяет mtime файла не чаще одного раза в 3 секунды на файл; устаревшие записи вытесняются автоматически

Для разработки установите STATIC_REVALIDATE=on, чтобы сервер всегда отдавал свежее содержимое. При желании также установите STATIC_MAX_AGE=off, чтобы полностью отключить кэширование в браузере.

Устранение неполадок

Сервер продолжает отдавать устаревшие файлы

По умолчанию кэш содержимого в памяти не проверяет, изменились ли файлы на диске. Установите STATIC_REVALIDATE=on во время разработки, чтобы включить ревалидацию по mtime — сервер обнаруживает изменения файлов автоматически (в течение 3 секунд).

Браузер продолжает показывать устаревшие файлы

Если сервер возвращает свежее содержимое, но браузер по-прежнему показывает старую версию, виноват собственный кэш браузера. Установите STATIC_MAX_AGE=off, чтобы перестать отправлять заголовки кэширования, или выполните полную перезагрузку в браузере (Shift+F5 или Cmd+Shift+R).

Файлы отдаются с `application/octet-stream`

OxPHP использует расширение файла для определения MIME-типа. Если расширение отсутствует или не распознано, используется запасной вариант application/octet-stream. Добавьте файлу правильное расширение или убедитесь, что ваш фреймворк явно устанавливает заголовок Content-Type в ответах PHP.

Большие файлы работают медленно

Файлы размером больше 1 MiB не кэшируются в памяти. Они передаются потоком с диска при каждом запросе либо, если они сжимаемые и меньше 3 MiB, считываются и сжимаются заново при каждом запросе от клиента, принимающего сжатие. Для очень больших файлов разместите CDN перед OxPHP, чтобы кэшировать их на границе сети. Как вариант, реорганизуйте свои ресурсы так, чтобы часто отдаваемые файлы оставались меньше 1 MiB.

Возвращаются ответы 304, когда вы ожидаете 200

304 означает, что у клиента уже есть текущая версия. Это корректное поведение. Если во время разработки нужно принудительно получить свежий ответ, установите STATIC_MAX_AGE=off, чтобы перестать отправлять заголовки ETag и Last-Modified.

Пример Docker

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "8080:80" volumes: - ./src:/var/www/html environment: - DOCUMENT_ROOT=/var/www/html/public - ENTRY_FILE=index.php - STATIC_MAX_AGE=1y

Рекомендации

  • Используйте длинные TTL с именами файлов, обходящими кэш в продакшене (например, app.a1b2c3.js). Установите STATIC_MAX_AGE=1y для максимального кэширования в браузере и CDN.
  • Устанавливайте STATIC_REVALIDATE=on во время разработки, чтобы сервер обнаруживал изменения файлов автоматически. При желании также установите STATIC_MAX_AGE=off, чтобы обойти кэширование в браузере.
  • Размещайте CDN перед OxPHP для высоконагруженных сайтов. Заголовки ETag, Last-Modified и Cache-Control работают со всеми крупными провайдерами CDN.
  • Доверьте хэширование ресурсов своему сборщику. Фреймворки вроде Vite и Laravel Mix генерируют имена файлов с хэшами автоматически, что делает длинные TTL кэша безопасными.

Промпты для вашего агента

Эти промпты рассчитаны на агента с подключённым MCP-сервером документации OxPHP — нужные страницы он прочитает сам.

prompt
Настрой кэширование статических файлов OxPHP под мою сборку ассетов. Прочитай страницу `features/static-files` через MCP-сервер документации OxPHP. Проверь, выдаёт ли сборка в этом репозитории имена файлов с хешем, и дай значения `STATIC_MAX_AGE` и `STATIC_REVALIDATE` для продакшена и для разработки, объяснив, что каждое из них меняет для браузера, который пришёл повторно.

Смотрите также

Нашли ошибку? Сообщите →