Fichiers statiques

OxPHP sert les fichiers statiques directement depuis la racine du document, sans invoquer PHP. Chaque fichier est servi avec une détection automatique du type MIME, un cache en mémoire pour un accès répété rapide, et une gestion complète du cache HTTP : ETags, requêtes conditionnelles et requêtes Range pour les téléchargements partiels.

Fonctionnement

Lorsqu'une requête correspond à un fichier statique :

  1. Fichier trouvé — la couche de routage résout le chemin de l'URL vers un fichier sur le disque
  2. Détection MIME — le type de contenu est déterminé à partir de l'extension du fichier
  3. Vérification du cache — le cache de fichiers est consulté avant de toucher au système de fichiers
  4. Vérification conditionnelle — si la requête porte un en-tête If-None-Match ou If-Modified-Since, OxPHP évalue la condition et peut renvoyer 304 Not Modified sans transmettre de corps
  5. Vérification Range — si une requête GET ou HEAD porte un en-tête Range, OxPHP répond avec 206 Partial Content : GET ne reçoit que la plage d'octets demandée, HEAD reçoit les mêmes en-têtes de plage sans corps
  6. Réponse — les fichiers jusqu'à 1 Mio sont servis depuis le cache en mémoire ; les fichiers plus volumineux sont diffusés en flux directement depuis le disque

Configuration

Variable Valeur par défaut Description
STATIC_MAX_AGE 30d Cache-Control: max-age pour les fichiers statiques. Accepte 30s, 5m, 2h, 30d, 1w, 1y, un simple nombre de secondes (par ex. 3600), ou off pour désactiver entièrement les en-têtes de cache. Remplace STATIC_CACHE_TTL, déprécié.
STATIC_REVALIDATE off Réglez sur on pour activer la revalidation par mtime sur le cache de contenu en mémoire (revérifie chaque fichier au plus une fois toutes les 3 secondes ; les modifications deviennent visibles dans cette fenêtre). Remplace STATIC_CACHE, déprécié (où off avait le sens inverse).

Détection MIME

Les types MIME sont déterminés automatiquement à partir de l'extension du fichier. Si aucun type ne peut être déterminé, le serveur se rabat sur application/octet-stream. Les correspondances courantes incluent :

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

Mise en cache des fichiers

OxPHP utilise un cache en mémoire pour réduire les I/O disque des fichiers fréquemment demandés :

  • Les fichiers jusqu'à 1 Mio (1 048 576 octets) sont lus en mémoire et mis en cache. Le budget total du cache est de 64 Mio (67 108 864 octets). Lorsque ce budget est dépassé, les entrées les moins récemment utilisées sont évincées pour libérer de la place.
  • Les fichiers de plus de 1 Mio sont toujours diffusés en flux directement depuis le disque. L'en-tête Content-Length est renseigné à partir des métadonnées du fichier, afin que le client connaisse d'emblée la taille totale.

Le cache de fichiers est peuplé lors de la première requête vers chaque fichier et conservé lors des requêtes suivantes. Par défaut, les entrées du cache persistent jusqu'à ce qu'elles soient évincées par la politique LRU.

Revalidation du contenu

Réglez STATIC_REVALIDATE=on pour activer la revalidation basée sur le mtime. Dans ce mode, le serveur revérifie l'heure de modification d'un fichier mis en cache avec un appel système stat() au plus une fois toutes les 3 secondes par fichier, et non à chaque requête. Si le fichier a changé sur le disque, l'entrée périmée est évincée et le fichier est relu automatiquement. Dans la fenêtre de 3 secondes, une entrée mise en cache est servie directement depuis la mémoire sans appel système, si bien que le coût est amorti plutôt que payé à chaque requête. Les modifications sur le disque deviennent visibles en 3 secondes.

Développement

Activez STATIC_REVALIDATE=on en développement pour voir les modifications de fichiers sans redémarrer le serveur. Laissez-le désactivé en production (la valeur par défaut off) pour un débit maximal sans aucune surcharge d'appel système par requête.

Cache HTTP

Cache-Control

Lorsque STATIC_MAX_AGE est défini (la valeur par défaut est 30d), chaque réponse de fichier statique inclut un en-tête Cache-Control :

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

La valeur max-age correspond au TTL converti en secondes. Réglez STATIC_MAX_AGE=off pour omettre entièrement cet en-tête.

ETag et Last-Modified

Chaque réponse de fichier statique inclut :

  • ETag — un ETag fort au format "<size>-<mtime_hex>", dérivé de la taille du fichier et de sa dernière heure de modification. Un validateur fort satisfait aussi If-Range, si bien que les téléchargements interrompus peuvent reprendre en toute sécurité. Lorsqu'une réponse est servie compressée en brotli, le tag est affaibli en W/"…" — les octets compressés constituent une représentation différente, et un tag faible permet encore de revalider (304) mais empêche de mélanger des fragments compressés et non compressés lors d'une reprise.
  • Last-Modified — une date HTTP au format RFC 7231 basée sur l'heure de modification du fichier

Ces en-têtes permettent aux navigateurs et aux CDN de valider les copies mises en cache sans retélécharger le fichier.

Requêtes conditionnelles (304)

OxPHP évalue les en-têtes de requête conditionnelle pour éviter d'envoyer un contenu de fichier inchangé :

  • If-None-Match — le client envoie l'ETag qu'il a en cache. S'il correspond au fichier actuel, OxPHP renvoie 304 Not Modified sans corps.
  • If-Modified-Since — le client envoie un horodatage. Si le fichier n'a pas été modifié depuis cette date, OxPHP renvoie 304.

If-None-Match a la priorité sur If-Modified-Since conformément à la RFC 7232. Pour les fichiers déjà présents dans le cache en mémoire, la vérification conditionnelle s'effectue sans aucune I/O disque.

Requêtes Range (206)

Les réponses de fichiers statiques annoncent Accept-Ranges: bytes, et les requêtes GET avec un en-tête Range à plage unique ne reçoivent que les octets demandés :

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

Cela permet le déplacement dans <video>/<audio> dans les navigateurs, les téléchargements reprenables (wget -c, gestionnaires de téléchargement) et le chargement partiel de PDF. Les trois formes de plage de la RFC 9110 sont prises en charge : bytes=N-M, bytes=N- (de l'offset jusqu'à la fin) et bytes=-N (les N derniers octets).

  • Une plage qui ne peut être satisfaite (début au-delà de la fin du fichier) renvoie 416 Range Not Satisfiable avec Content-Range: bytes */<size>.
  • If-Range est honoré : lorsque le client envoie l'ETag (ou la date Last-Modified) de sa copie partielle et que le fichier a changé depuis, OxPHP renvoie la réponse complète 200 plutôt qu'un fragment incohérent. La forme date n'est acceptée qu'une fois la seconde de modification du fichier entièrement écoulée — un fichier tout juste écrit pourrait changer à nouveau dans la même seconde sans faire évoluer la date, il ne s'agit donc pas encore d'un validateur fort (RFC 9110).
  • Les requêtes comportant plusieurs plages (bytes=0-1,4-5) reçoivent le fichier complet en 200 OK — les réponses multipart/byteranges ne sont pas générées.
  • Les requêtes HEAD avec un en-tête Range reçoivent les mêmes en-têtes 206/Content-Range que GET, sans corps, à l'image de nginx et Apache.
  • Les plages et la compression sont mutuellement exclusives. Pour les clients qui acceptent le brotli, la gestion des plages est désactivée sur les représentations qui seraient servies compressées, et les réponses compressées n'annoncent pas Accept-Ranges — un téléchargement repris pourrait sinon coller des octets non compressés sur un préfixe compressé. Seuls les fichiers servis depuis le cache en mémoire (jusqu'à 1 Mio) sont un jour compressés, si bien que les plages fonctionnent toujours pour le contenu qui en a réellement besoin : vidéo, archives, images et tout fichier diffusé en flux depuis le disque. Les réponses pour les fichiers éligibles à la compression portent toujours Vary: Accept-Encoding — même servies non compressées — afin que les caches partagés gardent les variantes distinctes.
  • Les réponses 206 ne sont jamais compressées, et la gestion des plages ne s'applique pas aux réponses PHP — uniquement aux fichiers statiques.

Exemple : reprendre un téléchargement interrompu avec curl :

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

Désactiver la mise en cache

Il existe deux couches de cache indépendantes et une variable pour chacune :

Variable Contrôle Effet de off
STATIC_MAX_AGE=off Cache navigateur (en-têtes HTTP) Aucun en-tête Cache-Control, ETag ou Last-Modified envoyé
STATIC_REVALIDATE=on Cache serveur en mémoire Revérifie le mtime du fichier au plus une fois toutes les 3 s par fichier ; les entrées périmées sont évincées automatiquement

En développement, réglez STATIC_REVALIDATE=on pour que le serveur serve toujours un contenu à jour. Vous pouvez éventuellement régler aussi STATIC_MAX_AGE=off pour empêcher entièrement la mise en cache par le navigateur.

Dépannage

Le serveur continue de servir des fichiers périmés

Par défaut, le cache de contenu en mémoire ne vérifie pas si les fichiers ont changé sur le disque. Réglez STATIC_REVALIDATE=on en développement pour activer la revalidation par mtime — le serveur détecte automatiquement les modifications de fichiers (dans un délai de 3 secondes).

Le navigateur continue de servir des fichiers périmés

Si le serveur renvoie un contenu à jour mais que le navigateur affiche encore l'ancienne version, c'est le propre cache du navigateur qui est en cause. Réglez STATIC_MAX_AGE=off pour cesser d'envoyer des en-têtes de cache, ou utilisez le rechargement forcé de votre navigateur (Shift+F5 ou Cmd+Shift+R).

Les fichiers sont servis avec `application/octet-stream`

OxPHP utilise l'extension du fichier pour déterminer le type MIME. Si une extension est absente ou non reconnue, il se rabat sur application/octet-stream. Ajoutez la bonne extension à votre fichier, ou assurez-vous que votre framework définit explicitement l'en-tête Content-Type dans les réponses PHP.

Les gros fichiers semblent lents

Les fichiers de plus de 1 Mio sont diffusés en flux depuis le disque à chaque requête et ne sont pas mis en cache en mémoire. Pour les très gros fichiers, placez un CDN devant OxPHP afin de les mettre en cache en périphérie. Vous pouvez aussi restructurer vos ressources pour que les fichiers fréquemment servis restent sous la barre des 1 Mio.

Des réponses 304 sont renvoyées là où vous attendez 200

Un 304 signifie que le client possède déjà la version actuelle. C'est le comportement attendu. Si vous devez forcer une réponse fraîche en développement, réglez STATIC_MAX_AGE=off pour cesser d'envoyer les en-têtes ETag et Last-Modified.

Exemple 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

Bonnes pratiques

  • Utilisez de longs TTL avec des noms de fichiers à cache-busting en production (par ex. app.a1b2c3.js). Réglez STATIC_MAX_AGE=1y pour une mise en cache maximale par le navigateur et le CDN.
  • Réglez STATIC_REVALIDATE=on en développement afin que le serveur détecte automatiquement les modifications de fichiers. Vous pouvez éventuellement régler aussi STATIC_MAX_AGE=off pour contourner le cache du navigateur.
  • Placez un CDN devant OxPHP pour les sites à fort trafic. Les en-têtes ETag, Last-Modified et Cache-Control fonctionnent avec tous les grands fournisseurs de CDN.
  • Laissez votre outil de build gérer le hachage des ressources. Des frameworks comme Vite et Laravel Mix génèrent automatiquement des noms de fichiers hachés, ce qui rend les longs TTL de cache sûrs.

Voir aussi

  • Compression — Compression Brotli pour les réponses de fichiers statiques compressibles
  • Routage — comment les chemins d'URL sont résolus vers des fichiers sur le disque
  • Référence de configuration — liste complète des variables d'environnement