Nginx location : priorité et ordre de correspondance
Nginx ne lit pas vos blocs location de haut en bas pour s’arrêter au premier qui convient. Ce seul malentendu est à l’origine de la plupart des rapports de bug « mon bloc location ne fonctionne pas ». Pour les locations de type préfixe, la position d’un bloc dans le fichier n’a strictement aucun effet : nginx les compare toutes et retient la correspondance la plus longue.
La véritable priorité des location nginx est une séquence figée en quatre étapes :
- Correspondance exacte. Si un
location = /pathest égal à l’URI, nginx l’utilise et s’arrête. Pas de comparaison de préfixes, pas de regex. - Préfixe le plus long. Toutes les locations de type préfixe (
location /pathetlocation ^~ /path) par lesquelles l’URI commence sont comparées. La plus longue est mémorisée, pas encore utilisée. - Le court-circuit
^~. Si le préfixe mémorisé porte^~, nginx saute entièrement la phase regex et utilise ce bloc. - Les regex, dans l’ordre du fichier. Sinon, les locations
~et~*sont essayées dans l’ordre où elles apparaissent dans la configuration, et la première correspondance l’emporte. Si aucune ne correspond, le préfixe mémorisé à l’étape 2 est utilisé.
Deux de ces règles tirent en sens inverse : les préfixes sont choisis par longueur sans égard à l’ordre, les regex par ordre sans égard à la longueur. Lire une configuration de haut en bas ne fera jamais apparaître ce conflit. Si vous voulez la réponse pour votre propre fichier plutôt que pour les exemples ci-dessous, collez-le dans le testeur nginx location gratuit : il rejoue cette séquence et montre à quelle étape chaque bloc perdant a été éliminé. Il tourne dans votre navigateur : une configuration de production collée ici ne quitte jamais la page. Chaque règle de correspondance décrite ici a été vérifiée sur un nginx 1.27.5 en fonctionnement, et non reprise d’articles de seconde main.
La priorité des location nginx en un coup d’œil
Cinq modificateurs participent à la correspondance d’URI, plus un qui n’y participe pas.
| Modificateur | Syntaxe | Correspond par | Arrête la phase regex | Usage typique |
|---|---|---|---|---|
= | location = /path | Égalité | Oui | Chemins critiques comme / ou /favicon.ico |
^~ | location ^~ /path | Commence par | Oui, s’il s’agit du préfixe le plus long | Répertoires qui ne doivent jamais atteindre une regex |
~ | location ~ regex | PCRE, sensible à la casse | Non | Routage par extension quand la casse compte |
~* | location ~* regex | PCRE, insensible à la casse | Non | Routage par extension quand la casse ne compte pas |
| (aucun) | location /path | Commence par | Non | Routage général par chemin |
@ | location @name | Jamais mis en correspondance avec l’URI | — | Cibles de error_page et try_files |
La règle empirique : une correspondance exacte bat tout le reste, une regex bat un préfixe, et les préfixes se départagent à la longueur. La seule exception est ^~, et elle est plus étroite qu’il n’y paraît.
Ce tableau n’est un classement qu’au sens le plus lâche. ^~ y figure au-dessus de ~, et pourtant un bloc ^~ perd régulièrement face à une regex, parce que le modificateur n’est consulté que sur le préfixe qui a déjà gagné à la longueur.
L’algorithme de sélection en quatre étapes
L’ordre de correspondance des location nginx s’apprend le plus facilement à partir d’une configuration qui met en jeu toutes les règles à la fois. Celle de la documentation nginx mérite d’être connue par cœur :
server {
location = / { return 200 "A\n"; }
location / { return 200 "B\n"; }
location /documents/ { return 200 "C\n"; }
location ^~ /images/ { return 200 "D\n"; }
location ~* \.(gif|jpg|jpeg)$ { return 200 "E\n"; }
}
Cinq requêtes, cinq réponses différentes :
| Requête | Gagnant | Pourquoi |
|---|---|---|
/ | A | Correspondance exacte. La recherche s’arrête immédiatement. |
/index.html | B | Aucune regex n’a correspondu, donc le préfixe mémorisé est utilisé. |
/documents/document.html | C | Préfixe plus long que /. |
/images/1.gif | D | ^~ a gagné l’étape des préfixes, la regex n’a donc jamais été évaluée. |
/documents/1.jpg | E | La regex a battu un préfixe plus long dépourvu de ^~. |
Comparez les deux dernières lignes. /images/ et /documents/ sont deux préfixes, ils correspondent tous les deux, et chacun est la correspondance la plus longue pour sa requête. Une requête part vers le bloc préfixe et l’autre vers la regex. La seule différence tient en deux caractères.
L’étape 2 est celle que l’on saute : nginx n’utilise pas le préfixe le plus long, il le mémorise. Le bloc est un candidat, et la phase regex peut encore lui prendre la requête. Seules les étapes 1, 3 et 4 mettent fin à la recherche.
Pourquoi le « préfixe le plus long » se mesure en caractères, pas en segments de chemin
La comparaison de préfixes est une simple comparaison de chaînes. Elle ignore que / sépare les segments de chemin, et elle ne s’arrête à aucune frontière. Avec cette configuration :
server {
location /static { }
location /static/ { }
}
une requête vers /staticfoo est servie par location /static. L’URI commence par ces sept caractères, elle correspond donc. /static/ ne correspond pas du tout, car il n’y a pas de barre oblique à cette position. Une requête vers /static/x prend l’autre chemin et va vers /static/, le plus long des deux.
La conséquence, c’est que location /static s’approprie aussi /static-backup, /staticfiles et tout ce qui commence par les mêmes lettres. Si vous vouliez désigner un répertoire, écrivez la barre oblique finale et ajoutez une location exacte pour le chemin nu :
server {
location /static/ { root /var/www; }
location = /static { return 301 /static/; }
}
La plupart des tutoriels utilisent des exemples de chemins bien rangés qui n’exposent jamais ce comportement, et c’est pourquoi il survit si souvent à la revue de code. Le testeur nginx location affiche tous les blocs qui ont correspondu ainsi que le nombre de caractères sur lesquels chacun a correspondu : on voit tout de suite quel préfixe avale ses voisins.
Ce que ^~ signifie vraiment (et ce qu’il ne signifie pas)
La description courante du modificateur ^~ de nginx location est « il fait passer ce bloc avant les regex ». C’est assez proche de la vérité pour être dangereux.
Ce que ^~ fait réellement : rien du tout pendant la comparaison des préfixes. Il n’allonge pas la correspondance, il n’élève pas le bloc au-dessus des autres préfixes, et il ne change pas quel préfixe est mémorisé. Il est examiné après coup, sur l’unique préfixe qui a déjà gagné à la longueur. Si ce gagnant porte ^~, la phase regex est sautée. Sinon, la phase regex se déroule normalement.
Ce qui veut dire qu’un préfixe simple plus long le désactive silencieusement :
server {
location ^~ /a/ { }
location /a/b/ { }
location ~ \.php$ { }
}
Une requête vers /a/b/x.php est traitée par ~ \.php$. /a/b/ est le préfixe correspondant le plus long, c’est donc lui que nginx mémorise ; son modificateur simple autorise la phase regex ; la regex correspond en premier et emporte la requête. Le bloc ^~ est toujours dans le fichier, il a toujours l’air protecteur, et il ne change rien à cette requête.
Changez l’URI en /a/x.php et la même configuration se comporte de façon complètement différente : maintenant ^~ /a/ est la correspondance la plus longue, la phase regex est sautée, et le bloc ^~ l’emporte. Le fichier n’a pas bougé, la requête a la même forme, et le résultat s’inverse.
Ce n’est pas une distinction théorique. L’usage canonique de ^~ consiste à tenir un répertoire accessible en écriture à l’écart d’un interpréteur :
server {
location ^~ /uploads/ { }
location ~ \.php$ { fastcgi_pass unix:/run/php-fpm.sock; }
}
Retirez le ^~ et une requête vers /uploads/evil.php file droit vers PHP-FPM. C’est la forme qui se cache derrière une longue série de rapports d’upload-to-RCE, et deux caractères séparent la version vulnérable de la version sûre. C’est aussi pour cela que le cas du « ^~ mis en échec » compte : ajouter un préfixe simple plus long comme location /uploads/thumbs/ rouvre la faille pour tout ce qui se trouve en dessous, et le diff qui fait cela a l’air parfaitement inoffensif.
Notez aussi la portée. ^~ ne supprime que les regex déclarées à son propre niveau ; il ne supprime jamais les regex imbriquées à l’intérieur de son propre bloc, et un ^~ posé sur une location imbriquée ne peut rien contre une regex déclarée au niveau serveur. Activez ou retirez le modificateur dans le testeur nginx location et regardez le gagnant changer ; les regex sautées restent dans le tableau, marquées comme telles.
Locations regex : l’ordre prime sur la spécificité
Une regex de location nginx utilise ~ pour une correspondance sensible à la casse et ~* pour une correspondance insensible à la casse. La correspondance par préfixe, elle, est toujours sensible à la casse sous Linux. (Sur les systèmes de fichiers insensibles à la casse comme macOS, nginx compare les préfixes sans tenir compte de la casse et force chaque location regex à se comporter comme ~*. Si vous développez sur Mac et déployez sous Linux, cette différence peut masquer une règle cassée jusqu’à la mise en production.)
Les regex sont évaluées dans l’ordre où elles apparaissent dans le fichier de configuration, et la première correspondance met fin à la recherche. C’est la règle qui piège tout le monde. La spécificité, la longueur et l’ancrage n’ont aucune influence sur cet ordre.
server {
location ~ ^/a { }
location ~ ^/a/b/c$ { }
}
Une requête vers /a/b/c est prise par ~ ^/a. Le second bloc correspond exactement à l’URI, il est bien plus précis, et il ne s’exécutera jamais pour aucune requête. C’est de la configuration morte que nginx -t accepte sans un mot.
D’où l’habitude à prendre : ordonnez les regex de la plus spécifique à la plus générale, et gardez la liste courte. Un motif large placé en haut rend inatteignable tout ce qui se trouve en dessous, et comme un diff qui ne fait que réordonner des lignes se lit comme anodin, la régression arrive plutôt lors d’un nettoyage que lors d’une nouvelle fonctionnalité.
L’ancrage mérite le même soin. location ~ /admin n’est ancré à aucune de ses deux extrémités : il cherche donc n’importe où dans l’URI et correspond volontiers à /public/admin/x. Écrivez ~ ^/admin quand vous visez le début. N’ancrer qu’à la fin, comme dans ~ \.php$, est normal et correct pour un routage par extension.
Quelques détails de PCRE qui prennent à revers les intuitions héritées de JavaScript :
- nginx compile les motifs de location avec PCRE, sans mode UTF ni mode multiligne : les motifs opèrent donc sur des octets et
^s’ancre uniquement au début de l’URI. - Le
$de PCRE correspond aussi juste avant un saut de ligne final. Une URI se terminant par%0Asatisfait toujours\.php$, ce qui est une manière connue de passer sous les règles fondées sur une extension de fichier. - Les constructions sans équivalent en JavaScript sont fréquentes en PCRE : groupes atomiques
(?>…), quantificateurs possessifsa*+, modificateurs en ligne comme(?i), classes POSIX comme[[:alpha:]], et échappements tels que\A,\z,\Ket\Q…\E. - Un motif contenant
{ou}doit être mis entre guillemets droits.location ~ ^/a{2}$échoue au chargement avecunknown directive "2}$", parce que l’accolade a terminé le token. Écrivezlocation ~ "^/a{2}$".
Les groupes de capture fonctionnent comme on l’espère, et $1 et suivants sont disponibles à l’intérieur du bloc :
upstream backend {
server 127.0.0.1:8080;
}
server {
location ~ ^/user/(\d+)/profile$ {
proxy_pass http://backend/profiles/$1;
}
}
Si vous en êtes encore à déboguer le motif lui-même plutôt que sa position dans le fichier, testez-le d’abord dans le testeur Regex ; l’aide-mémoire Regex couvre la syntaxe en profondeur.
L’étape que tout le monde oublie : la normalisation de l’URI
Avant qu’aucune location ne soit consultée, nginx réécrit la cible de la requête. Vos motifs sont comparés au chemin normalisé, pas aux octets arrivés sur le réseau. Presque aucun tutoriel n’en parle, et cela tranche un nombre surprenant de cas « ma location ne correspond pas ».
La normalisation fait quatre choses : elle détache la chaîne de requête, applique le décodage pour-cent au chemin, résout les segments . et .., et fusionne les barres obliques répétées.
| Cible de la requête | $uri normalisé | Remarque |
|---|---|---|
//a//x | /a/x | Barres obliques répétées fusionnées |
/a/../b/x | /b/x | .. résolu avant la correspondance |
/a/b%2F..%2Fzz | /a/zz | %2F se décode en un vrai séparateur et participe à la résolution |
/a/%2e%2e/b/x | /b/x | %2E se décode en un point qui participe lui aussi |
/a%20b/x | /a b/x | %20 devient une véritable espace |
/a+b/x | /a+b/x | + n’est pas une espace dans un chemin |
/a?x=/b | /a | La chaîne de requête est détachée en premier |
/a%3Fx=1 | /a?x=1 | %3F reste littéral ; la chaîne de requête est vide |
Trois caractères décodés font exception et sont réécrits tels quels sans être réinterprétés : %25, %23 et %3F. C’est pour cela que /a%3Fx=1 se retrouve avec un point d’interrogation à l’intérieur du chemin et rien dans $args.
Deux cibles n’atteignent jamais la sélection de location. Les segments .. qui remontent au-dessus de la racine et un échappement invalide comme %00 sont tous deux rejetés avec un 400 avant même le début de la correspondance.
Cela compte surtout pour la sécurité. Si vous utilisez un bloc location comme frontière de contrôle d’accès, le chemin que vous avez écrit est comparé au chemin résolu :
server {
location /a/ { }
location /b/ { }
}
Une requête vers /a/b%2F..%2Fzz ne reste pas sous /a/b/. Elle se normalise en /a/zz et est traitée par location /a/. Raisonner sur la cible brute plutôt que sur $uri donne ici la mauvaise réponse, et « mauvaise réponse » dans un contexte de contrôle d’accès porte un nom précis. Avant de compter sur location /admin pour protéger quoi que ce soit, vérifiez ce que vaut réellement le chemin normalisé : le testeur nginx location affiche la cible brute, le $uri normalisé et la chaîne de requête détachée sur trois lignes distinctes. Si vous n’avez besoin que de raisonner sur l’encodage lui-même, le décodeur et encodeur URL s’en charge isolément.
Dernière conséquence : la chaîne de requête ne participe jamais à la correspondance. location /search?q= ne peut pas correspondre à une requête vers /search?q=1, parce que la sélection ne voit jamais que /search. Pour brancher sur un paramètre, lisez $arg_name à l’intérieur du bloc.
Cinq configurations qui ne font pas ce que vous croyez
Un bloc ^~ battu par un préfixe simple plus long
Symptôme : un répertoire ^~ a l’air protégé, et une regex traite quand même les requêtes qui s’y trouvent.
Cause : ^~ n’est consulté que sur le préfixe qui a déjà gagné à la longueur. Un préfixe simple plus long est mémorisé à sa place, et il ne supprime rien.
Correctif : mettez ^~ sur le préfixe le plus long aussi, ou supprimez ce préfixe plus long.
# Broken: /a/b/x.php goes to the regex
location ^~ /a/ { }
location /a/b/ { }
location ~ \.php$ { }
# Fixed: /a/b/x.php goes to ^~ /a/b/
location ^~ /a/ { }
location ^~ /a/b/ { }
location ~ \.php$ { }
Un préfixe sans barre oblique finale qui attrape ses voisins
Symptôme : un bloc prévu pour un seul répertoire sert aussi des chemins qui commencent simplement par les mêmes lettres.
Cause : la correspondance par préfixe compare des caractères, pas des segments de chemin : location /app correspond donc aussi à /application.
Correctif : écrivez la barre oblique finale, et ajoutez location = /app si le chemin nu doit lui aussi être traité.
La regex spécifique placée sous la regex large
Symptôme : une règle précise ne se déclenche jamais, et aucune erreur n’apparaît nulle part. Cause : les regex sont essayées dans l’ordre du fichier et la première correspondance met fin à la recherche : tout ce qui se trouve sous un motif large est inatteignable. Correctif : déplacez le motif spécifique au-dessus du motif large, ou resserrez le motif large avec un ancrage.
Une regex écrite derrière ^~
Symptôme : une règle deny se charge sans erreur et ne bloque rien.
Cause : ^~ prend un préfixe littéral, pas un motif. nginx ne proteste jamais ; le bloc ne correspond simplement jamais à une URI.
Correctif : utilisez le modificateur regex.
# Broken: matches nothing, loads without error
location ^~ "\.php$" { deny all; }
# Fixed
location ~ \.php$ { deny all; }
Croire que la chaîne de requête participe
Symptôme : une location contenant ? ne correspond jamais.
Cause : la chaîne de requête est détachée pendant la normalisation et la sélection de location s’exécute sur le chemin seul.
Correctif : faites correspondre sur le chemin et inspectez $arg_name à l’intérieur du bloc.
location /search {
if ($arg_q = "") { return 400; }
}
Débogage : savoir quel bloc l’a réellement emporté
Le journal de débogage donne la réponse qui fait autorité, et c’est aussi le plus lourd à mettre en place. Il exige un binaire compilé avec le support du débogage, vérifiez donc d’abord :
nginx -V 2>&1 | grep -o with-debug
Activez-le ensuite et cherchez la ligne qui nomme le bloc sélectionné :
error_log /var/log/nginx/debug.log debug;
grep "using configuration" /var/log/nginx/debug.log
Une sonde par en-tête de réponse va plus vite et ne réclame pas de build de débogage. Étiquetez chaque candidat, puis relisez les en-têtes :
location ^~ /uploads/ {
add_header X-Debug-Location "uploads-caret" always;
return 204;
}
location ~ \.php$ {
add_header X-Debug-Location "php-regex" always;
return 204;
}
curl -sI --path-as-is 'http://localhost/uploads/evil.php' | grep -i x-debug-location
--path-as-is a son importance : sans lui, curl résout obligeamment les .. à votre place et vous finissez par tester une URI différente de celle que vous visiez. Si vous assemblez quelque chose de plus élaboré, le générateur de commande cURL écrit les options pour vous, et l’aide-mémoire curl couvre le reste. Quand la sonde renvoie une redirection ou un 404 au lieu de votre en-tête, la référence des codes HTTP vous dira en général quel module l’a produite.
nginx -T imprime la configuration entièrement fusionnée, fichiers include compris. C’est ainsi que vous découvrez dans quel ordre vos regex se trouvent vraiment une fois six fichiers assemblés, ce qui est rarement l’ordre dans lequel elles apparaissent dans le fichier que vous éditiez.
nginx -T | grep -n "location"
Reproduisez la panne dans le testeur nginx location avant de toucher au serveur. Travailler sur une configuration non déployée va plus vite qu’un cycle de rechargement, et le tableau de décision nomme l’étape à laquelle chaque bloc a été éliminé au lieu de vous laisser le deviner.
Locations imbriquées, try_files et ce qu’ils ne changent pas
Les locations imbriquées exécutent le même algorithme un niveau plus bas. Une fois qu’une location de type préfixe l’a emporté, nginx descend dans ses enfants et y recommence la recherche, ce qui veut dire qu’une regex imbriquée est essayée avant les regex du niveau parent :
server {
location ~ \.php$ { }
location /a/ {
location ~ \.php$ { return 200 "nested\n"; }
}
}
/a/x.php est traité par le bloc imbriqué. L’imbrication a aussi un effet moins évident : elle peut rendre inatteignable un préfixe globalement plus long, car on ne descend que dans le gagnant de chaque niveau. Si location /a/bb/ est imbriqué dans location /a/, et qu’un voisin location /a/b se trouve au niveau extérieur, une requête vers /a/bb/x part vers /a/b. La comparaison extérieure a lieu en premier, et /a/b la remporte. Une descente qui ne trouve rien ne revient pas non plus en arrière : le parent garde la requête.
try_files et rewrite ne font pas partie de la sélection. Ils s’exécutent à l’intérieur du bloc qui a déjà gagné, et ils ne peuvent pas revenir en arrière pour changer ce choix. Si une requête n’arrive jamais au bloc qui contient votre try_files, la directive est sans objet, et le coupable habituel est une regex ~ \.php$ qui prend la requête avant que le bloc préfixe n’ait eu son tour. La seule exception à connaître : une redirection interne (rewrite … last, ou un saut via error_page) relance la correspondance, si bien que l’URI réécrite est confrontée depuis le début à la liste des locations.
Enfin, le 301 que vous obtenez en demandant un répertoire sans barre oblique finale n’est pas un échec de correspondance. Deux mécanismes distincts le produisent. Si une location dont le nom se termine par / porte proxy_pass ou une autre directive *_pass, une requête vers le même chemin sans la barre oblique reçoit un 301 pendant la sélection, avant qu’aucune regex ne soit évaluée, et avec la chaîne de requête préservée :
server {
location /user/ { proxy_pass http://backend/; }
}
# GET /user?x=1 -> 301 to /user/?x=1
Ajouter location = /user supprime cette redirection. Séparément, le module de fichiers statiques émet son propre 301 quand un chemin correspond à un vrai répertoire sur le disque, ce qui dépend de votre système de fichiers et non de votre configuration.
FAQ
Quels sont les cinq modificateurs de location nginx ?
Les cinq modificateurs de location nginx sont = pour la correspondance exacte, ^~ pour un préfixe qui saute la phase regex, aucun modificateur pour un préfixe ordinaire, ~ pour une regex sensible à la casse et ~* pour une regex insensible à la casse. Une sixième forme, location @name, ne participe jamais à la correspondance d’URI et n’existe que comme cible pour error_page et try_files.
Une correspondance exacte = rend-elle nginx plus rapide ?
Une location en correspondance exacte met fin à la recherche immédiatement, en sautant le balayage des préfixes et toute évaluation de regex. Le gain est réel, mais trop petit pour être perceptible. Cela vaut la peine pour des points d’entrée sollicités des milliers de fois par seconde, comme les health checks ou /favicon.ico. Un empilement de blocs = pour des pages ordinaires coûte plus en complexité de configuration qu’il ne rapporte.
Puis-je écrire un modificateur de location sans espace, comme ~*^/api ?
Oui. location ~*^/api/ et location ~* ^/api/ signifient exactement la même chose, car nginx détache le modificateur du début du nom et reconnaît d’abord le modificateur le plus long : ~* est donc reconnu avant ~. Gardez quand même l’espace. Un modificateur collé finit par se lire comme une partie du motif, et c’est en relecture que l’erreur se paie.
Quelle est la différence entre root et alias dans un bloc location ?
root ajoute l’URI entière au répertoire, tandis qu’alias remplace le préfixe correspondant par ce répertoire. Avec location /static/ { root /var/www; }, une requête vers /static/x.css cherche /var/www/static/x.css ; passez à alias /var/www/assets/; et elle cherche /var/www/assets/x.css. Avec alias, donnez une barre oblique finale à la fois à la location et au chemin, ou à aucun des deux.
Une même requête peut-elle correspondre à plusieurs blocs location ?
Plusieurs blocs peuvent correspondre, mais un seul traite la requête. nginx compare toutes les locations de type préfixe et, si nécessaire, toutes les locations regex, puis confie la requête à l’unique gagnant. Les directives ne sont pas héritées des blocs perdants : tout ce dont vous avez besoin partout doit vivre au niveau server ou http, ou être répété.
nginx -t me dit-il quelle location va correspondre ?
Non. nginx -t vérifie la syntaxe et la validité de la configuration ; il ne simule jamais de requête, il ne rapporte donc rien sur l’ordre de correspondance. Pour savoir quel bloc possède une URI, lisez le journal de débogage, ajoutez un en-tête de réponse temporaire, ou collez la configuration dans le testeur nginx location et lisez pourquoi chaque bloc a gagné ou perdu.