Skip to content
Volver al blog
Tutoriales

Prioridad de location en Nginx: orden de coincidencia

Cómo elige nginx un bloque location: el orden exacto de =, ^~, ~ y prefijos, más los errores que rompen configuraciones, con un tester online gratis.

12 min de lectura

Prioridad de location en Nginx: orden de coincidencia

Nginx no lee tus bloques location de arriba abajo para quedarse con el primero que encaje. Ese único malentendido está detrás de la mayoría de los reportes de “mi bloque location no funciona”. En las locations de prefijo, el lugar que ocupa un bloque dentro del archivo no influye en nada: nginx las compara todas y se queda con la coincidencia más larga.

La prioridad real de location en nginx es una secuencia fija de cuatro pasos:

  1. Coincidencia exacta. Si un location = /path es igual al URI, nginx lo usa y se detiene. Sin comparación de prefijos, sin regex.
  2. Prefijo más largo. Se comparan todas las locations de prefijo (location /path y location ^~ /path) con las que empieza el URI. La más larga se recuerda, todavía no se usa.
  3. El cortocircuito de ^~. Si el prefijo recordado lleva ^~, nginx se salta por completo la fase de regex y usa ese bloque.
  4. Regex, en el orden del archivo. Si no, las locations ~ y ~* se prueban en el orden en que aparecen en la configuración, y gana la primera que coincida. Si ninguna coincide, se usa el prefijo recordado en el paso 2.

Dos de esas reglas tiran en direcciones opuestas: los prefijos se eligen por longitud sin importar el orden, y las regex por orden sin importar la longitud. Leer una configuración de arriba abajo nunca va a sacar ese conflicto a la luz. Si quieres la respuesta para tu propio archivo y no para los ejemplos de abajo, pégalo en el probador de nginx location: reproduce esta secuencia y muestra en qué etapa quedó eliminado cada bloque perdedor. Funciona en tu navegador, así que una configuración de producción pegada aquí nunca sale de la página. Cada regla de coincidencia descrita aquí se verificó contra un nginx 1.27.5 en funcionamiento, no se copió de artículos de segunda mano.

La prioridad de location en nginx de un vistazo

Cinco modificadores participan en la coincidencia de URI, más uno que no.

ModificadorSintaxisCoincide porDetiene la fase de regexUso típico
=location = /pathIgualdadRutas calientes como / o /favicon.ico
^~location ^~ /pathEmpieza porSí, si es el prefijo más largoDirectorios que nunca deben llegar a una regex
~location ~ regexPCRE, distingue mayúsculasNoEnrutado por extensión cuando importan las mayúsculas
~*location ~* regexPCRE, sin distinguir mayúsculasNoEnrutado por extensión cuando no importan
(ninguno)location /pathEmpieza porNoEnrutado general por ruta
@location @nameNunca coincide por URIDestinos de error_page y try_files

La regla práctica: una coincidencia exacta le gana a todo, una regex le gana a un prefijo, y los prefijos se ganan entre sí por longitud. La única excepción es ^~, y es más estrecha de lo que parece.

Esa tabla es un ranking solo en el sentido más laxo. ^~ aparece por encima de ~, y aun así un bloque ^~ pierde con frecuencia frente a una regex, porque el modificador solo se consulta sobre el prefijo que ya ganó por longitud.

El algoritmo de selección en cuatro pasos

La forma más rápida de aprender el orden de coincidencia de location en nginx es leer una configuración que ejercite todas las reglas a la vez. Este es el ejemplo de la documentación de nginx, y conviene tenerlo a mano:

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"; }
}

Cinco peticiones, cinco respuestas distintas:

PeticiónGanadorPor qué
/ACoincidencia exacta. La búsqueda termina de inmediato.
/index.htmlBNinguna regex coincidió, así que se usa el prefijo recordado.
/documents/document.htmlCPrefijo más largo que /.
/images/1.gifD^~ ganó la etapa de prefijos, así que la regex nunca se ejecutó.
/documents/1.jpgELa regex le ganó a un prefijo más largo que no llevaba ^~.

Compara las dos últimas filas. /images/ y /documents/ son prefijos, los dos coinciden y los dos son la coincidencia más larga para su petición. Aun así, una se queda en el bloque de prefijo y la otra se va a la regex; lo único que los distingue es el ^~ del cuarto bloque.

El paso 2 es el que la gente se salta: nginx no usa el prefijo más largo, lo recuerda. El bloque queda como candidato, y la fase de regex todavía puede quitarle la petición. Solo los pasos 1, 3 y 4 terminan la búsqueda.

Por qué el “prefijo más largo” se mide en caracteres y no en segmentos de ruta

La comparación de prefijos es una comparación de cadenas y nada más. No sabe que / separa segmentos de ruta, ni se detiene en un límite. Con esta configuración:

server {
    location /static  { }
    location /static/ { }
}

una petición a /staticfoo la atiende location /static. El URI empieza con esos siete caracteres, así que coincide. /static/ no coincide en absoluto, porque en esa posición no hay barra. Una petición a /static/x va para el otro lado y se queda con /static/, el más largo de los dos.

La consecuencia es que location /static también se apropia de /static-backup, /staticfiles y de cualquier otra cosa que empiece con las mismas letras. Si querías un directorio, escribe la barra final y agrega una location exacta para la ruta desnuda:

server {
    location /static/ { root /var/www; }
    location = /static { return 301 /static/; }
}

La mayoría de los tutoriales usa ejemplos de rutas bien ordenadas que nunca dejan esto al descubierto, y por eso sobrevive tantas veces a la revisión de código. El probador de nginx location lista cada bloque que coincidió junto con la cantidad de caracteres que consumió, así que un prefijo que se traga a sus hermanos salta a la vista.

Qué significa ^~ en realidad (y qué no)

La descripción habitual del modificador ^~ de location en nginx es “hace que este bloque tenga prioridad sobre las regex”. Es lo bastante parecido a la verdad como para ser peligroso.

Lo que ^~ hace de verdad: nada en absoluto durante la comparación de prefijos. No alarga la coincidencia, no eleva el bloque por encima de otros prefijos y no cambia qué prefijo se recuerda. Se consulta después, sobre el único prefijo que ya ganó por longitud. Si ese ganador lleva ^~, la fase de regex se salta. Si no lo lleva, la fase de regex corre con normalidad.

El resultado es que un prefijo simple más largo lo desactiva en silencio:

server {
    location ^~ /a/    { }
    location /a/b/     { }
    location ~ \.php$  { }
}

Una petición a /a/b/x.php la maneja ~ \.php$. /a/b/ es el prefijo coincidente más largo, así que eso es lo que nginx recuerda; su modificador simple permite la fase de regex; la regex coincide primero y se lleva la petición. El bloque ^~ sigue en el archivo, sigue pareciendo protector, y no tiene la menor influencia sobre esta petición.

Cambia el URI a /a/x.php y la misma configuración se comporta de forma completamente distinta: ahora ^~ /a/ es la coincidencia más larga, la fase de regex se salta y gana el bloque ^~. Es el mismo archivo y casi la misma petición, con el resultado opuesto.

Esto no es una distinción académica. El uso canónico de ^~ es mantener un directorio con permiso de escritura lejos de un intérprete:

server {
    location ^~ /uploads/ { }
    location ~ \.php$     { fastcgi_pass unix:/run/php-fpm.sock; }
}

Quita el ^~ y una petición a /uploads/evil.php va directo a PHP-FPM. Ese patrón está detrás de una larga lista de reportes de subida de archivos que terminan en RCE, y la diferencia entre vulnerable y seguro son dos caracteres. También es la razón por la que importa el caso del ^~ derrotado: agregar un prefijo simple más largo como location /uploads/thumbs/ reabre el agujero para todo lo que cuelga debajo, y ese diff pasa una revisión sin que nadie levante una ceja.

Fíjate también en el alcance. ^~ solo suprime las regex declaradas en su propio nivel; nunca suprime las regex anidadas dentro de su propio bloque, y un ^~ en una location anidada no puede proteger frente a una regex declarada a nivel de servidor. Activa y desactiva el modificador en el probador de nginx location y observa cómo cambia el ganador: cada regex que se saltó queda marcada como tal en la tabla.

Locations con regex: el orden le gana a la especificidad

Una location con regex en nginx usa ~ para coincidencia sensible a mayúsculas y ~* para insensible. La coincidencia por prefijo, en cambio, siempre distingue mayúsculas en Linux. (En sistemas de archivos que no distinguen mayúsculas, como los de macOS, nginx compara los prefijos sin distinguir mayúsculas y obliga a que toda location con regex se comporte como ~*. Si desarrollas en una Mac y despliegas en Linux, esa diferencia puede esconder una regla rota hasta que llega a producción.)

La regla que agarra a la gente desprevenida es que las regex se evalúan en el orden en que aparecen en el archivo de configuración, y la primera coincidencia termina la búsqueda. La especificidad, la longitud y el anclaje no influyen en el orden.

server {
    location ~ ^/a       { }
    location ~ ^/a/b/c$  { }
}

Una petición a /a/b/c se la lleva ~ ^/a. El segundo bloque coincide con el URI de forma exacta, es mucho más preciso, y no se va a ejecutar jamás para ninguna petición. Es configuración muerta que nginx -t acepta sin decir una palabra.

Así que conviene ordenar las regex de más específica a menos específica y mantener la lista corta. Un patrón amplio cerca del inicio deja inalcanzable todo lo que viene después, y como un diff que solo reordena líneas se lee como inofensivo, la regresión suele llegar durante una limpieza y no durante una funcionalidad nueva.

El anclaje merece el mismo cuidado. location ~ /admin no está anclado en ningún extremo, así que busca en cualquier parte del URI y coincide tan campante con /public/admin/x. Escribe ~ ^/admin cuando te refieras al inicio. Anclar solo al final, como en ~ \.php$, es normal y correcto para enrutar por extensión.

Algunos detalles de PCRE que las intuiciones formadas en JavaScript entienden mal:

  • nginx compila los patrones de location con PCRE, sin modo UTF ni multilínea, así que los patrones operan sobre bytes y ^ ancla únicamente al inicio del URI.
  • El $ de PCRE también coincide justo antes de un salto de línea final. Un URI que termina en %0A sigue satisfaciendo \.php$, que es una forma conocida de colarse por reglas basadas en la extensión del archivo.
  • Las construcciones sin equivalente en JavaScript son comunes en PCRE: grupos atómicos (?>…), cuantificadores posesivos a*+, modificadores en línea como (?i), clases POSIX como [[:alpha:]], y escapes como \A, \z, \K y \Q…\E.
  • Un patrón que contenga { o } debe ir entre comillas. location ~ ^/a{2}$ no llega a cargar y falla con unknown directive "2}$", porque la llave terminó el token. Escribe location ~ "^/a{2}$".

Los grupos de captura funcionan como esperarías, y $1 en adelante están disponibles dentro del bloque:

upstream backend {
    server 127.0.0.1:8080;
}

server {
    location ~ ^/user/(\d+)/profile$ {
        proxy_pass http://backend/profiles/$1;
    }
}

Si todavía estás depurando el patrón en sí y no su posición en el archivo, pruébalo primero en el probador de regex; el cheat sheet de regex cubre la sintaxis a fondo.

El paso que todos se saltan: la normalización del URI

Antes de consultar cualquier location, nginx reescribe el destino de la petición. Tus patrones se comparan contra la ruta normalizada, no contra los bytes que llegaron por la red. Casi ningún tutorial lo menciona, y esa reescritura resuelve una cantidad sorprendente de casos de “mi location no coincide”.

La normalización hace cuatro cosas: separa la cadena de consulta, aplica la decodificación porcentual a la ruta, resuelve los segmentos . y .., y colapsa las barras repetidas.

Destino de la petición$uri normalizadoNota
//a//x/a/xBarras repetidas fusionadas
/a/../b/x/b/x.. se resuelve antes de la coincidencia
/a/b%2F..%2Fzz/a/zz%2F se decodifica a un separador real y entra en la resolución
/a/%2e%2e/b/x/b/x%2E se decodifica a un punto que también participa
/a%20b/x/a b/x%20 se vuelve un espacio real
/a+b/x/a+b/x+ no es un espacio en una ruta
/a?x=/b/aLa cadena de consulta se separa primero
/a%3Fx=1/a?x=1%3F se queda literal; la cadena de consulta queda vacía

Tres caracteres decodificados son la excepción y se escriben tal cual, sin reinterpretarse: %25, %23 y %3F. Por eso /a%3Fx=1 termina con un signo de interrogación dentro de la ruta y nada en $args.

Dos destinos nunca llegan a la selección de location. Los segmentos .. que suben por encima de la raíz y un escape inválido como %00 se rechazan con 400 antes de que empiece la coincidencia.

Donde más pesa esto es en seguridad. Si usas un bloque location como frontera de control de acceso, la ruta que escribiste se compara contra la ruta resuelta:

server {
    location /a/ { }
    location /b/ { }
}

Una petición a /a/b%2F..%2Fzz no se queda debajo de /a/b/. Se normaliza a /a/zz y la maneja location /a/. Razonar sobre el destino crudo en vez de sobre $uri da la respuesta equivocada, y “respuesta equivocada” en un contexto de control de acceso tiene un nombre propio. Antes de confiar en que location /admin protege algo, confirma cuál es realmente la ruta normalizada: el probador de nginx location muestra el destino crudo, el $uri normalizado y la cadena de consulta ya separada, cada uno en su propia fila. Si lo que te falta entender es la codificación en sí, el decodificador de URL hace ese trabajo aparte.

Una consecuencia más: la cadena de consulta nunca participa en la coincidencia. location /search?q= no puede coincidir con una petición a /search?q=1, porque la selección solo llega a ver /search. Para ramificar según un parámetro, lee $arg_name dentro del bloque.

Cinco configuraciones que no hacen lo que crees

Un bloque ^~ que pierde ante un prefijo simple más largo

Síntoma: un directorio con ^~ parece protegido, y aun así una regex atiende las peticiones que caen dentro. Causa: ^~ solo se consulta sobre el prefijo que ya ganó por longitud. En su lugar se recuerda un prefijo simple más largo, y ese no suprime nada. Solución: pon ^~ también en el prefijo más largo, o elimina el prefijo más largo.

# 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 prefijo sin barra final que se traga a sus hermanos

Síntoma: un bloque pensado para un directorio también atiende rutas que apenas empiezan con las mismas letras. Causa: la coincidencia por prefijo compara caracteres, no segmentos de ruta, así que location /app también coincide con /application. Solución: escribe la barra final, y agrega location = /app si la ruta desnuda también necesita atención.

La regex específica puesta debajo de la amplia

Síntoma: una regla precisa nunca se dispara, y no aparece ningún error por ningún lado. Causa: las regex se prueban en el orden del archivo y la primera coincidencia termina la búsqueda, así que todo lo que está debajo de un patrón amplio es inalcanzable. Solución: mueve el patrón específico por encima del amplio, o ajusta el amplio con un ancla.

Una regex escrita después de ^~

Síntoma: una regla deny carga sin problemas y no bloquea nada. Causa: ^~ toma un prefijo literal, no un patrón. nginx nunca se queja; el bloque sencillamente nunca coincide con un URI. Solución: usa el modificador de regex.

# Broken: matches nothing, loads without error
location ^~ "\.php$" { deny all; }

# Fixed
location ~ \.php$ { deny all; }

Dar por hecho que la cadena de consulta participa

Síntoma: una location que contiene ? nunca coincide. Causa: la cadena de consulta se separa durante la normalización y la selección de location corre solo contra la ruta. Solución: haz coincidir la ruta e inspecciona $arg_name dentro del bloque.

location /search {
    if ($arg_q = "") { return 400; }
}

Depuración: averigua qué bloque ganó de verdad

El log de depuración es la respuesta definitiva, y también el que más cuesta poner en marcha. Necesita un binario compilado con soporte de depuración, así que revisa primero:

nginx -V 2>&1 | grep -o with-debug

Después actívalo y filtra la línea que nombra el bloque seleccionado:

error_log /var/log/nginx/debug.log debug;
grep "using configuration" /var/log/nginx/debug.log

Las sondas por cabecera de respuesta son más rápidas y no necesitan una compilación con depuración. Etiqueta cada candidato y lee las cabeceras de la respuesta:

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 importa: sin esa opción, curl resuelve amablemente los .. por ti y terminas probando un URI distinto del que querías. Si estás armando algo más elaborado, el generador de comandos cURL escribe los flags por ti, y la chuleta de curl cubre el resto. Cuando la sonda devuelve una redirección o un 404 en lugar de tu cabecera, la referencia de códigos de estado HTTP suele decirte qué módulo lo produjo.

nginx -T imprime la configuración completamente fusionada, con archivos include y todo. Así averiguas en qué orden quedaron realmente tus regex después de ensamblar seis archivos, que casi nunca es el orden que veías en el archivo que estabas editando.

nginx -T | grep -n "location"

Reproduce el fallo en el probador de nginx location antes de tocar el servidor. Iterar contra una configuración que todavía no desplegaste es más rápido que un ciclo de recarga, y la tabla de decisión te dice en qué etapa quedó eliminado cada bloque.

Locations anidadas, try_files y lo que no cambian

Las locations anidadas ejecutan el mismo algoritmo un nivel más abajo. Una vez que gana una location de prefijo, nginx desciende a sus hijas y repite la búsqueda ahí, así que una regex anidada se prueba antes que las regex del nivel padre:

server {
    location ~ \.php$ { }
    location /a/ {
        location ~ \.php$ { return 200 "nested\n"; }
    }
}

/a/x.php lo maneja el bloque anidado. El anidamiento también tiene un efecto menos obvio: puede volver inalcanzable un prefijo que a nivel global es más largo, porque solo se desciende al ganador de cada nivel. Si location /a/bb/ está anidada dentro de location /a/, y una hermana location /a/b está en el nivel exterior, una petición a /a/bb/x va a /a/b. La comparación exterior ocurre primero, y /a/b la gana. Un descenso que no encuentra nada tampoco retrocede: la petición se queda con el padre.

try_files y rewrite no son parte de la selección. Se ejecutan dentro del bloque que ya ganó, y no pueden volver atrás para cambiarlo. Si una petición nunca llega al bloque que contiene tu try_files, la directiva es irrelevante, y el culpable habitual es una regex ~ \.php$ que se lleva la petición antes de que el bloque de prefijo tenga siquiera su turno. Hay una sola excepción: una redirección interna (rewrite … last, o un salto de error_page) reinicia la coincidencia, así que el URI reescrito se resuelve otra vez contra la lista de locations desde el principio.

El 301 que recibes al pedir un directorio sin barra final tampoco es un fallo de coincidencia. Lo producen dos mecanismos distintos. Si una location cuyo nombre termina en / lleva proxy_pass u otra directiva *_pass, una petición a la misma ruta sin la barra se responde con un 301 durante la selección, antes de evaluar cualquier regex y conservando la cadena de consulta:

server {
    location /user/ { proxy_pass http://backend/; }
}
# GET /user?x=1  ->  301 to /user/?x=1

Agregar location = /user suprime esa redirección. El segundo mecanismo es el módulo de archivos estáticos, que emite su propio 301 cuando una ruta se resuelve a un directorio real en disco, y eso depende de tu sistema de archivos, no de tu configuración.

Preguntas frecuentes

¿Cuáles son los cinco modificadores de location en nginx?

Los cinco modificadores de location en nginx son = para coincidencia exacta, ^~ para un prefijo que se salta la fase de regex, ningún modificador para un prefijo común, ~ para una regex sensible a mayúsculas y ~* para una insensible. Una sexta forma, location @name, nunca participa en la coincidencia por URI y existe solo como destino para error_page y try_files.

¿Una coincidencia exacta con = hace que nginx sea más rápido?

Una location de coincidencia exacta termina la búsqueda de inmediato, saltándose el escaneo de prefijos y toda evaluación de regex. El ahorro es real, pero demasiado pequeño para notarlo. Vale la pena escribirla para endpoints que reciben miles de peticiones por segundo, como los health checks o /favicon.ico. Un montón de bloques = para páginas comunes cuesta más en complejidad de configuración de lo que aporta.

¿Puedo escribir un modificador de location sin espacio, como ~*^/api?

Sí. location ~*^/api/ y location ~* ^/api/ significan exactamente lo mismo, porque nginx separa el modificador del inicio del nombre y busca primero el modificador más largo, así que ~* se reconoce antes que ~. Aun así, deja el espacio. Un modificador pegado se lee como parte del patrón, y en una revisión de código eso se malinterpreta con facilidad.

¿Cuál es la diferencia entre root y alias dentro de un bloque location?

root agrega el URI completo al directorio, mientras que alias reemplaza con él el prefijo que coincidió. Con location /static/ { root /var/www; } una petición a /static/x.css busca /var/www/static/x.css; cambia a alias /var/www/assets/; y busca /var/www/assets/x.css. Con alias, o le pones barra final tanto a la location como a la ruta, o no se la pones a ninguna de las dos.

¿Una misma petición puede coincidir con más de un bloque location?

Pueden coincidir varios bloques, pero exactamente uno atiende la petición. nginx compara todas las locations de prefijo y, cuando hace falta, todas las de regex, y luego entrega la petición al único ganador. Las directivas no se heredan de los bloques perdedores: lo que necesites en todas partes tiene que vivir a nivel de server o de http, o repetirse.

¿nginx -t me dice qué location va a coincidir?

No. nginx -t revisa la sintaxis y la validez de la configuración; nunca simula una petición, así que no reporta nada sobre el orden de coincidencia. Para saber qué bloque se queda con un URI, lee el log de depuración, agrega una cabecera de respuesta temporal, o pega la configuración en el probador de nginx location y lee por qué ganó o perdió cada bloque.

Etiquetas: nginx web-server devops regex configuration

Artículos relacionados

Ver todos los artículos