Pliki statyczne

OxPHP serwuje pliki statyczne prosto z katalogu głównego dokumentów, bez uruchamiania PHP. Każdy plik jest serwowany z automatycznym wykrywaniem typu MIME, buforem w pamięci zapewniającym szybki dostęp przy powtarzających się żądaniach oraz pełnym buforowaniem HTTP: nagłówkami ETag, żądaniami warunkowymi i żądaniami Range dla pobrań częściowych.

Jak to działa

Gdy żądanie pasuje do pliku statycznego:

  1. Dopasowanie pliku — warstwa routingu rozwiązuje ścieżkę URL do pliku na dysku
  2. Wykrywanie typu MIME — typ zawartości jest ustalany na podstawie rozszerzenia pliku
  3. Sprawdzenie bufora — bufor plików jest sprawdzany przed sięgnięciem do systemu plików
  4. Sprawdzenie warunkowe — jeśli żądanie zawiera If-None-Match lub If-Modified-Since, OxPHP ocenia warunek i może zwrócić 304 Not Modified bez wysyłania ciała odpowiedzi
  5. Sprawdzenie zakresu — jeśli żądanie GET lub HEAD zawiera nagłówek Range, OxPHP odpowiada kodem 206 Partial Content: GET otrzymuje tylko żądany zakres bajtów, a HEAD te same nagłówki zakresu bez ciała
  6. Odpowiedź — pliki do 1 MiB są serwowane z bufora w pamięci; większe pliki są strumieniowane bezpośrednio z dysku

Konfiguracja

Zmienna Wartość domyślna Opis
STATIC_MAX_AGE 30d Cache-Control: max-age dla plików statycznych. Przyjmuje 30s, 5m, 2h, 30d, 1w, 1y, samą liczbę sekund (np. 3600) lub off, aby całkowicie wyłączyć nagłówki buforowania. Zastępuje przestarzałą zmienną STATIC_CACHE_TTL.
STATIC_REVALIDATE off Ustaw na on, aby włączyć rewalidację na podstawie mtime dla bufora zawartości w pamięci (ponowne sprawdzenie każdego pliku najwyżej raz na 3 sekundy; zmiany stają się widoczne w tym oknie czasowym). Zastępuje przestarzałą zmienną STATIC_CACHE (w której off miało odwrotne znaczenie).

Wykrywanie typu MIME

Typy MIME są ustalane automatycznie na podstawie rozszerzenia pliku. Jeśli nie da się ustalić typu, serwer używa domyślnie application/octet-stream. Do typowych odwzorowań należą:

Rozszerzenie Content-Type
.html text/html
.css text/css
.js text/javascript
.json application/json
.png image/png
.svg image/svg+xml
.woff2 font/woff2

Buforowanie plików

OxPHP używa bufora w pamięci, aby ograniczyć operacje I/O na dysku dla często żądanych plików:

  • Pliki do 1 MiB (1 048 576 bajtów) są wczytywane do pamięci i buforowane. Całkowity budżet bufora wynosi 64 MiB (67 108 864 bajtów). Po przekroczeniu budżetu najdawniej używane wpisy są usuwane, aby zwolnić miejsce.
  • Pliki większe niż 1 MiB są zawsze strumieniowane bezpośrednio z dysku. Nagłówek Content-Length jest ustawiany na podstawie metadanych pliku, dzięki czemu klient zna z góry całkowity rozmiar.

Bufor plików jest wypełniany przy pierwszym żądaniu każdego pliku i utrzymywany między kolejnymi żądaniami. Domyślnie wpisy bufora pozostają, dopóki nie zostaną usunięte przez politykę LRU.

Rewalidacja zawartości

Ustaw STATIC_REVALIDATE=on, aby włączyć rewalidację na podstawie mtime. W tym trybie serwer ponownie sprawdza czas modyfikacji buforowanego pliku wywołaniem systemowym stat() najwyżej raz na 3 sekundy dla każdego pliku, a nie przy każdym żądaniu. Jeśli plik zmienił się na dysku, nieaktualny wpis jest usuwany, a plik zostaje automatycznie wczytany ponownie. W oknie 3-sekundowym buforowany wpis jest serwowany prosto z pamięci bez wywołania systemowego, więc koszt jest amortyzowany, a nie płacony przy każdym żądaniu. Zmiany na dysku stają się widoczne w ciągu 3 sekund.

Praca deweloperska

Ustaw STATIC_REVALIDATE=on podczas pracy deweloperskiej, aby widzieć zmiany w plikach bez restartowania serwera. W środowisku produkcyjnym pozostaw tę zmienną nieustawioną (domyślne off), aby uzyskać maksymalną przepustowość przy zerowym narzucie wywołań systemowych na żądanie.

Buforowanie HTTP

Cache-Control

Gdy ustawiona jest zmienna STATIC_MAX_AGE (domyślnie 30d), każda odpowiedź z plikiem statycznym zawiera nagłówek Cache-Control:

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

Wartość max-age to TTL przeliczony na sekundy. Ustaw STATIC_MAX_AGE=off, aby całkowicie pominąć ten nagłówek.

ETag i Last-Modified

Każda odpowiedź z plikiem statycznym zawiera:

  • ETag — silny ETag w formacie "<size>-<mtime_hex>", wyprowadzony z rozmiaru pliku i czasu ostatniej modyfikacji. Silny walidator spełnia też warunek If-Range, dzięki czemu przerwane pobrania można bezpiecznie wznowić. Gdy odpowiedź jest serwowana skompresowana algorytmem brotli, znacznik jest osłabiany do postaci W/"…" — skompresowane bajty to inna reprezentacja, a słaby znacznik nadal umożliwia rewalidację (304), ale zapobiega mieszaniu skompresowanych i nieskompresowanych fragmentów przy wznawianiu.
  • Last-Modified — data HTTP zgodna z RFC 7231, oparta na czasie modyfikacji pliku

Nagłówki te pozwalają przeglądarkom i sieciom CDN walidować buforowane kopie bez ponownego pobierania pliku.

Żądania warunkowe (304)

OxPHP ocenia nagłówki żądań warunkowych, aby uniknąć wysyłania niezmienionej zawartości pliku:

  • If-None-Match — klient wysyła ETag, który ma w buforze. Jeśli pasuje on do bieżącego pliku, OxPHP zwraca 304 Not Modified bez ciała odpowiedzi.
  • If-Modified-Since — klient wysyła znacznik czasu. Jeśli plik nie był modyfikowany od tego momentu, OxPHP zwraca 304.

If-None-Match ma pierwszeństwo przed If-Modified-Since zgodnie z RFC 7232. Dla plików znajdujących się już w buforze w pamięci sprawdzenie warunkowe przebiega bez żadnych operacji I/O na dysku.

Żądania Range (206)

Odpowiedzi z plikami statycznymi ogłaszają Accept-Ranges: bytes, a żądania GET z pojedynczym zakresem w nagłówku Range otrzymują tylko żądane bajty:

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

Umożliwia to przewijanie elementów <video>/<audio> w przeglądarkach, wznawialne pobrania (wget -c, menedżery pobierania) oraz częściowe wczytywanie plików PDF. Obsługiwane są wszystkie trzy formy zakresu z RFC 9110: bytes=N-M, bytes=N- (od przesunięcia do końca) oraz bytes=-N (ostatnie N bajtów).

  • Zakres, którego nie da się spełnić (początek poza końcem pliku), zwraca 416 Range Not Satisfiable z nagłówkiem Content-Range: bytes */<size>.
  • If-Range jest respektowany: gdy klient wysyła ETag (lub datę Last-Modified) swojej częściowej kopii, a plik zmienił się od tamtej pory, OxPHP zwraca pełną odpowiedź 200 zamiast niepasującego fragmentu. Forma z datą jest akceptowana dopiero po pełnym upływie sekundy modyfikacji pliku — świeżo zapisany plik mógłby zmienić się ponownie w tej samej sekundzie bez zmiany daty, więc data nie jest jeszcze silnym walidatorem (RFC 9110).
  • Żądania z wieloma zakresami (bytes=0-1,4-5) otrzymują cały plik jako 200 OK — odpowiedzi multipart/byteranges nie są generowane.
  • Żądania HEAD z nagłówkiem Range otrzymują te same nagłówki 206/Content-Range co GET, ale bez ciała, zgodnie z zachowaniem nginx i Apache.
  • Zakresy i kompresja wzajemnie się wykluczają. Dla klientów akceptujących brotli obsługa zakresów jest wyłączana dla reprezentacji, które byłyby serwowane skompresowane, a skompresowane odpowiedzi nie ogłaszają Accept-Ranges — w przeciwnym razie wznowione pobranie mogłoby doszyć nieskompresowane bajty do skompresowanego prefiksu. Kompresowane są wyłącznie pliki serwowane z bufora w pamięci (do 1 MiB), więc zakresy zawsze działają dla zawartości, która faktycznie ich potrzebuje: wideo, archiwów, obrazów i każdego pliku strumieniowanego z dysku. Odpowiedzi dla plików kwalifikujących się do kompresji zawsze niosą nagłówek Vary: Accept-Encoding — nawet gdy są serwowane nieskompresowane — dzięki czemu współdzielone bufory rozróżniają warianty.
  • Odpowiedzi 206 nigdy nie są kompresowane, a obsługa zakresów nie dotyczy odpowiedzi PHP — tylko plików statycznych.

Przykład: wznowienie przerwanego pobrania za pomocą curl:

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

Wyłączanie buforowania

Istnieją dwie niezależne warstwy bufora i jedna zmienna dla każdej z nich:

Zmienna Kontroluje Efekt off
STATIC_MAX_AGE=off Bufor przeglądarki (nagłówki HTTP) Nie są wysyłane nagłówki Cache-Control, ETag ani Last-Modified
STATIC_REVALIDATE=on Bufor serwera w pamięci Ponowne sprawdzanie mtime pliku najwyżej raz na 3 s dla każdego pliku; nieaktualne wpisy usuwane automatycznie

W środowisku deweloperskim ustaw STATIC_REVALIDATE=on, aby serwer zawsze serwował świeżą zawartość. Opcjonalnie ustaw też STATIC_MAX_AGE=off, aby całkowicie wyłączyć buforowanie w przeglądarce.

Rozwiązywanie problemów

Serwer wciąż serwuje nieaktualne pliki

Domyślnie bufor zawartości w pamięci nie sprawdza, czy pliki zmieniły się na dysku. Ustaw STATIC_REVALIDATE=on podczas pracy deweloperskiej, aby włączyć rewalidację na podstawie mtime — serwer automatycznie wykrywa zmiany plików (w ciągu 3 sekund).

Przeglądarka wciąż serwuje nieaktualne pliki

Jeśli serwer zwraca świeżą zawartość, a przeglądarka nadal pokazuje starą wersję, winowajcą jest własny bufor przeglądarki. Ustaw STATIC_MAX_AGE=off, aby przestać wysyłać nagłówki buforowania, albo użyj twardego przeładowania w przeglądarce (Shift+F5 lub Cmd+Shift+R).

Pliki są serwowane z typem `application/octet-stream`

OxPHP ustala typ MIME na podstawie rozszerzenia pliku. Jeśli rozszerzenia brakuje lub nie jest rozpoznane, używany jest domyślnie application/octet-stream. Dodaj poprawne rozszerzenie do pliku albo zadbaj o to, aby framework jawnie ustawiał nagłówek Content-Type w odpowiedziach PHP.

Duże pliki wydają się wolne

Pliki większe niż 1 MiB są strumieniowane z dysku przy każdym żądaniu i nie są buforowane w pamięci. W przypadku bardzo dużych plików umieść przed OxPHP sieć CDN, aby buforowała je na brzegu. Alternatywnie zrestrukturyzuj zasoby tak, aby często serwowane pliki mieściły się poniżej 1 MiB.

Zwracane są odpowiedzi 304, gdy oczekujesz 200

Kod 304 oznacza, że klient ma już bieżącą wersję. To poprawne zachowanie. Jeśli podczas pracy deweloperskiej musisz wymusić świeżą odpowiedź, ustaw STATIC_MAX_AGE=off, aby przestać wysyłać nagłówki ETag i Last-Modified.

Przykład dla Dockera

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

Dobre praktyki

  • Używaj długich TTL wraz z nazwami plików odpornymi na buforowanie (cache-busting) w środowisku produkcyjnym (np. app.a1b2c3.js). Ustaw STATIC_MAX_AGE=1y, aby uzyskać maksymalne buforowanie w przeglądarce i sieci CDN.
  • Ustaw STATIC_REVALIDATE=on podczas pracy deweloperskiej, aby serwer automatycznie wykrywał zmiany plików. Opcjonalnie ustaw też STATIC_MAX_AGE=off, aby pominąć buforowanie w przeglądarce.
  • Umieść sieć CDN przed OxPHP w przypadku witryn o dużym ruchu. Nagłówki ETag, Last-Modified i Cache-Control współpracują ze wszystkimi głównymi dostawcami CDN.
  • Pozwól narzędziu budującemu zająć się hashowaniem zasobów. Frameworki takie jak Vite i Laravel Mix automatycznie generują nazwy plików z hashami, dzięki czemu długie TTL bufora są bezpieczne.

Zobacz również

  • Kompresja — kompresja Brotli dla kompresowalnych odpowiedzi z plikami statycznymi
  • Routing — jak ścieżki URL są rozwiązywane do plików na dysku
  • Dokumentacja konfiguracji — pełna lista zmiennych środowiskowych