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

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 берётся из метаданных файла, поэтому клиент заранее знает полный размер.

Кэш файлов заполняется при первом запросе к каждому файлу и сохраняется между последующими запросами. По умолчанию записи кэша хранятся до тех пор, пока не будут вытеснены политикой 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, поэтому прерванные загрузки можно безопасно возобновлять. Когда ответ отдаётся сжатым brotli, тег ослабляется до W/"…" — сжатые байты представляют собой другое представление, и слабый тег по-прежнему позволяет выполнять ревалидацию (304), но предотвращает смешивание сжатых и несжатых фрагментов при возобновлении.
  • Last-Modified — HTTP-дата по RFC 7231 на основе времени изменения файла

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

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

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

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

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

Запросы 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.
  • Диапазоны и сжатие взаимоисключающи. Для клиентов, принимающих brotli, обработка диапазонов отключается для представлений, которые были бы отданы в сжатом виде, а сжатые ответы не объявляют Accept-Ranges — иначе возобновлённая загрузка могла бы приклеить несжатые байты к сжатому префиксу. Сжимаются только файлы, отдаваемые из кэша в памяти (до 1 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 передаются потоком с диска при каждом запросе и не кэшируются в памяти. Для очень больших файлов разместите 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.10.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 кэша безопасными.

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