Priorytet bloków location w Nginx: kolejność dopasowania
Nginx nie czyta bloków location od góry do dołu i nie zatrzymuje się na pierwszym, który pasuje. To jedno nieporozumienie stoi za większością zgłoszeń w stylu „mój blok location nie działa”. W przypadku prefiksowych bloków location ich miejsce w pliku nie ma znaczenia. Nginx porównuje je wszystkie i zachowuje najdłuższe dopasowanie.
Rzeczywisty priorytet bloków location w nginx to stała, czterostopniowa sekwencja:
- Dopasowanie dokładne. Jeśli
location = /pathjest równe URI, nginx używa tego bloku i kończy. Nie porównuje prefiksów i nie uruchamia fazy regex. - Najdłuższy prefiks. Porównywane są wszystkie prefiksowe bloki
location(location /pathorazlocation ^~ /path), od których zaczyna się URI. Najdłuższy zostaje zapamiętany, a nie od razu użyty. - Zwarcie na
^~. Jeśli zapamiętany prefiks niesie modyfikator^~, nginx pomija całą fazę regex i używa tego bloku. - Regex, w kolejności z pliku. W przeciwnym razie bloki
locationz~i~*są sprawdzane w kolejności występowania w konfiguracji, a wygrywa pierwsze dopasowanie. Jeśli żadne nie pasuje, użyty zostaje prefiks zapamiętany w kroku 2.
Dwie z tych reguł ciągną w przeciwne strony: prefiksy wybiera się według długości, bez względu na kolejność, a wyrażenia regularne według kolejności, bez względu na długość. Czytanie konfiguracji od góry do dołu nigdy tego konfliktu nie ujawni. Jeśli potrzebna jest odpowiedź dla własnego pliku, a nie dla poniższych przykładów, wystarczy wkleić go do darmowego testera bloków location w nginx. Narzędzie odtwarza tę sekwencję i pokazuje, na którym etapie odpadł każdy przegrany blok. Narzędzie działa w przeglądarce, więc wklejona konfiguracja produkcyjna nigdy nie opuszcza strony. Każda opisana tu reguła dopasowania została sprawdzona na działającym nginx 1.27.5, a nie przepisana z artykułów z drugiej ręki.
Priorytet bloków location w nginx w skrócie
W dopasowywaniu URI bierze udział pięć modyfikatorów, plus jeden, który nie bierze.
| Modyfikator | Składnia | Dopasowuje przez | Przerywa fazę regex | Typowe zastosowanie |
|---|---|---|---|---|
= | location = /path | Równość | Tak | Gorące ścieżki, takie jak / czy /favicon.ico |
^~ | location ^~ /path | Początek ścieżki | Tak, jeśli jest najdłuższym prefiksem | Katalogi, które nigdy nie mogą trafić do regex |
~ | location ~ regex | PCRE, z rozróżnianiem wielkości liter | Nie | Routing po rozszerzeniu, gdy wielkość liter ma znaczenie |
~* | location ~* regex | PCRE, bez rozróżniania wielkości liter | Nie | Routing po rozszerzeniu, gdy nie ma znaczenia |
| (brak) | location /path | Początek ścieżki | Nie | Ogólny routing po ścieżce |
@ | location @name | Nigdy nie dopasowuje URI | — | Cele dla error_page i try_files |
Reguła praktyczna: dopasowanie dokładne bije wszystko, regex bije prefiks, a prefiksy biją się nawzajem długością. Jedyny wyjątek to ^~, który działa węziej, niż się wydaje.
Ta tabela jest rankingiem tylko w najluźniejszym sensie. ^~ stoi w niej nad ~, a mimo to blok ^~ regularnie przegrywa z wyrażeniem regularnym, bo modyfikator jest sprawdzany wyłącznie na tym prefiksie, który już wygrał długością.
Czterostopniowy algorytm wyboru
Kolejności dopasowania location w nginx najłatwiej nauczyć się na konfiguracji, która uruchamia wszystkie reguły naraz. Ten przykład pochodzi z dokumentacji nginx:
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"; }
}
Pięć żądań, pięć różnych odpowiedzi:
| Żądanie | Zwycięzca | Dlaczego |
|---|---|---|
/ | A | Dopasowanie dokładne. Wyszukiwanie kończy się natychmiast. |
/index.html | B | Żaden regex nie pasował, więc użyty zostaje zapamiętany prefiks. |
/documents/document.html | C | Dłuższy prefiks niż /. |
/images/1.gif | D | ^~ wygrał etap prefiksów, więc regex w ogóle się nie uruchomił. |
/documents/1.jpg | E | Regex pokonał dłuższy prefiks, któremu brakowało ^~. |
Dwa ostatnie wiersze są prawie bliźniacze, a kończą się inaczej. /images/ i /documents/ to oba prefiksy, oba pasują, oba są najdłuższym dopasowaniem dla swojego żądania. Jedno żądanie trafia do bloku prefiksowego, drugie do wyrażenia regularnego. Cała różnica to dwa znaki.
Krok 2 to ta część, którą się pomija: nginx nie używa najdłuższego prefiksu, tylko go zapamiętuje. Blok jest kandydatem, a faza regex wciąż może mu żądanie odebrać. Wyszukiwanie kończą jedynie kroki 1, 3 i 4.
Dlaczego „najdłuższy prefiks” mierzy się w znakach, a nie w segmentach ścieżki
Porównanie prefiksów to zwykłe porównanie ciągów znaków. Nie wie, że / rozdziela segmenty ścieżki, i nie zatrzymuje się na granicy. Przy takiej konfiguracji:
server {
location /static { }
location /static/ { }
}
żądanie /staticfoo obsługuje location /static. URI zaczyna się od tych siedmiu znaków, więc pasuje. /static/ nie pasuje w ogóle, bo na tej pozycji nie ma ukośnika. Żądanie /static/x idzie w drugą stronę i trafia do /static/, dłuższego z tych dwóch.
Konsekwencja jest taka, że location /static przejmuje również /static-backup, /staticfiles i wszystko inne, co przypadkiem zaczyna się od tych samych liter. Jeśli chodziło o katalog, trzeba dopisać końcowy ukośnik i dodać dokładny blok location dla samej ścieżki:
server {
location /static/ { root /var/www; }
location = /static { return 301 /static/; }
}
Większość poradników używa schludnych przykładów ścieżek, które tego nigdy nie odsłaniają, i dlatego problem tak często przechodzi przez code review. Tester bloków location pokazuje wszystkie pasujące bloki i długość dopasowania w znakach, więc prefiks połykający rodzeństwo widać, zamiast się go domyślać.
Co ^~ naprawdę oznacza (a czego nie)
Popularny opis modyfikatora ^~ w nginx brzmi: „ten blok ma pierwszeństwo przed wyrażeniami regularnymi”. To na tyle bliskie prawdy, że bywa niebezpieczne.
Co ^~ robi naprawdę: nic a nic podczas porównywania prefiksów. Nie wydłuża dopasowania i nie zmienia tego, który prefiks zostanie zapamiętany. Jest sprawdzany dopiero potem, na tym jednym prefiksie, który już wygrał długością. Jeśli zwycięzca niesie ^~, faza regex zostaje pominięta. Jeśli nie niesie, faza regex przebiega normalnie.
Czyli dłuższy zwykły prefiks po cichu go wyłącza:
server {
location ^~ /a/ { }
location /a/b/ { }
location ~ \.php$ { }
}
Żądanie /a/b/x.php obsługuje ~ \.php$. /a/b/ jest najdłuższym pasującym prefiksem, więc to jego nginx zapamiętuje; jego zwykły modyfikator dopuszcza fazę regex; wyrażenie pasuje jako pierwsze i przejmuje żądanie. Blok ^~ nadal stoi w pliku, nadal wygląda ochronnie i nie ma na to żądanie najmniejszego wpływu.
Po zmianie URI na /a/x.php ta sama konfiguracja zachowuje się zupełnie inaczej: najdłuższym dopasowaniem jest teraz ^~ /a/, faza regex zostaje pominięta i wygrywa blok ^~. Plik się nie zmienił, żądanie ma ten sam kształt, a wynik jest przeciwny.
To nie jest rozróżnienie akademickie. Kanoniczne zastosowanie ^~ to trzymanie zapisywalnego katalogu z dala od interpretera:
server {
location ^~ /uploads/ { }
location ~ \.php$ { fastcgi_pass unix:/run/php-fpm.sock; }
}
Bez ^~ żądanie /uploads/evil.php idzie prosto do PHP-FPM. Taki właśnie kształt ma długa seria zgłoszeń typu upload-to-RCE. Dlatego przypadek „pokonanego ^~” ma znaczenie: dodanie dłuższego zwykłego prefiksu, na przykład location /uploads/thumbs/, ponownie otwiera dziurę dla wszystkiego, co leży pod nim, a diff, który to robi, wygląda całkowicie niewinnie.
Zasięg tego tłumienia też ma granice. ^~ działa wyłącznie na wyrażenia regularne zadeklarowane na własnym poziomie; nigdy nie tłumi wyrażeń zagnieżdżonych wewnątrz własnego bloku, a ^~ na zagnieżdżonym bloku location nie ochroni przed regex zadeklarowanym na poziomie serwera. Przełączenie modyfikatora w testerze bloków location od razu pokazuje, jak zmienia się zwycięzca. Narzędzie oznacza każde pominięte wyrażenie regularne, więc żadne nie znika po cichu z tabeli.
Bloki location z regex: kolejność bije precyzję
Wyrażenie regularne w bloku location w nginx używa ~ dla dopasowania z rozróżnianiem wielkości liter i ~* dla dopasowania bez rozróżniania. Dopasowanie prefiksowe jest za to na Linuksie zawsze wrażliwe na wielkość liter. (W systemach plików nierozróżniających wielkości liter, takich jak macOS, nginx porównuje prefiksy bez rozróżniania i zmusza każdy blok location z regex do zachowania jak ~*. Kiedy pisze się na Macu, a wdraża na Linuksie, ta różnica potrafi ukrywać zepsutą regułę aż do wdrożenia.)
Najczęściej zaskakuje reguła kolejności: wyrażenia regularne są sprawdzane w kolejności występowania w pliku konfiguracyjnym, a pierwsze dopasowanie kończy wyszukiwanie. Precyzja wzorca ani jego długość nic tu nie zmieniają.
server {
location ~ ^/a { }
location ~ ^/a/b/c$ { }
}
Żądanie /a/b/c przejmuje ~ ^/a. Drugi blok pasuje do URI dokładnie, jest znacznie precyzyjniejszy i nigdy nie wykona się dla żadnego żądania. To martwa konfiguracja, którą nginx -t przyjmuje bez słowa.
Stąd nawyk: wyrażenia regularne układa się od najbardziej szczegółowego do najogólniejszego, a listę trzyma się krótką. Szeroki wzorzec blisko góry sprawia, że wszystko pod nim staje się nieosiągalne. Diff, który tylko przestawia linie, czyta się przy tym jak niegroźny, więc regresja pojawia się raczej przy porządkach niż przy nowej funkcji.
Z zakotwiczeniem jest podobnie. location ~ /admin nie jest zakotwiczone z żadnej strony, więc szuka w dowolnym miejscu URI i ochoczo pasuje do /public/admin/x. Kiedy chodzi o początek, pisze się ~ ^/admin. Zakotwiczenie tylko na końcu, jak w ~ \.php$, jest przy routingu po rozszerzeniu normalne i poprawne.
Kilka szczegółów PCRE, które intuicja ukształtowana na JavaScripcie przekłamuje:
- nginx kompiluje wzorce z bloków
locationprzy użyciu PCRE, bez trybu UTF i bez trybu wieloliniowego, więc wzorce operują na bajtach, a^kotwiczy wyłącznie na początku URI. $w PCRE pasuje także tuż przed końcowym znakiem nowej linii. URI zakończone na%0Anadal spełnia\.php$, co jest znanym sposobem na obejście reguł opartych na rozszerzeniu pliku.- Konstrukcje bez odpowiednika w JavaScripcie są w PCRE częste: grupy atomowe
(?>…), kwantyfikatory zaborczea*+, modyfikatory wbudowane w rodzaju(?i), klasy POSIX takie jak[[:alpha:]]oraz sekwencje ucieczki\A,\z,\Ki\Q…\E. - Wzorzec zawierający
{lub}musi zostać ujęty w cudzysłów.location ~ ^/a{2}$nie wczytuje się i zgłaszaunknown directive "2}$", bo klamra zakończyła token. Trzeba napisaćlocation ~ "^/a{2}$".
Grupy przechwytujące działają zgodnie z oczekiwaniami, a $1 i kolejne są dostępne wewnątrz bloku:
upstream backend {
server 127.0.0.1:8080;
}
server {
location ~ ^/user/(\d+)/profile$ {
proxy_pass http://backend/profiles/$1;
}
}
Kiedy problemem jest sam wzorzec, a nie jego pozycja w pliku, sprawdza się go najpierw w testerze wyrażeń regularnych; składnię omawia szczegółowo ściągawka z wyrażeń regularnych.
Krok, który wszyscy pomijają: normalizacja URI
Zanim jakikolwiek blok location zostanie w ogóle sprawdzony, nginx przepisuje cel żądania. Wzorce porównuje się ze znormalizowaną ścieżką, a nie z bajtami, które przyszły po kablu. Prawie żaden poradnik o tym nie wspomina, a to rozstrzyga zaskakująco wiele przypadków „mój location nie pasuje”.
Normalizacja robi cztery rzeczy: odcina query string, dekoduje procentowo ścieżkę, rozwiązuje segmenty . i .. oraz scala powtórzone ukośniki.
| Cel żądania | Znormalizowane $uri | Uwaga |
|---|---|---|
//a//x | /a/x | Powtórzone ukośniki scalone |
/a/../b/x | /b/x | .. rozwiązane przed dopasowaniem |
/a/b%2F..%2Fzz | /a/zz | %2F dekoduje się do prawdziwego separatora i włącza do rozwiązywania |
/a/%2e%2e/b/x | /b/x | %2E dekoduje się do kropki, która też bierze udział |
/a%20b/x | /a b/x | %20 staje się prawdziwą spacją |
/a+b/x | /a+b/x | + nie jest spacją w ścieżce |
/a?x=/b | /a | Query string odcięty jako pierwszy |
/a%3Fx=1 | /a?x=1 | %3F pozostaje dosłowne; query string jest pusty |
Trzy zdekodowane znaki są wyjątkiem i zostają wypisane dosłownie, bez ponownej interpretacji: %25, %23 i %3F. Dlatego /a%3Fx=1 kończy ze znakiem zapytania wewnątrz ścieżki i z pustym $args.
Dwa cele nigdy nie docierają do wyboru bloku location. Segmenty .. wychodzące ponad katalog główny oraz nieprawidłowa sekwencja ucieczki, taka jak %00, są odrzucane kodem 400, zanim dopasowanie w ogóle się zacznie.
Dla bezpieczeństwa ma to konkretne konsekwencje. Jeśli blok location pełni funkcję granicy kontroli dostępu, zapisana ścieżka porównywana jest ze ścieżką rozwiązaną:
server {
location /a/ { }
location /b/ { }
}
Żądanie /a/b%2F..%2Fzz nie zostaje pod /a/b/. Normalizuje się do /a/zz i obsługuje je location /a/. Rozumowanie na podstawie surowego celu zamiast $uri daje tu złą odpowiedź, a „zła odpowiedź” w kontekście kontroli dostępu ma swoją własną nazwę. Zanim oprze się na location /admin ochronę czegokolwiek, trzeba potwierdzić, jak naprawdę wygląda znormalizowana ścieżka. Tester bloków location pokazuje surowy cel, znormalizowane $uri i odcięty query string jako trzy osobne wiersze, a kiedy chodzi wyłącznie o samo kodowanie, koder i dekoder URL obsłuży to w izolacji.
Wynika stąd jeszcze jedna rzecz: query string nigdy nie bierze udziału w dopasowaniu. location /search?q= nie może dopasować żądania /search?q=1, bo wybór widzi wyłącznie /search. Żeby rozgałęzić się po parametrze, czyta się $arg_name wewnątrz bloku.
Pięć konfiguracji, które nie robią tego, czego się po nich spodziewamy
Blok ^~ przegrywający z dłuższym zwykłym prefiksem
Objaw: katalog z ^~ wygląda na chroniony, a żądania w jego wnętrzu i tak obsługuje wyrażenie regularne.
Przyczyna: ^~ sprawdzane jest wyłącznie na prefiksie, który już wygrał długością. Zapamiętany zostaje dłuższy zwykły prefiks, a ten niczego nie tłumi.
Rozwiązanie: dodać ^~ również do dłuższego prefiksu albo usunąć dłuższy prefiks.
# 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$ { }
Prefiks bez końcowego ukośnika przejmujący rodzeństwo
Objaw: blok przeznaczony dla jednego katalogu obsługuje też ścieżki, które tylko zaczynają się od tych samych liter.
Przyczyna: dopasowanie prefiksowe porównuje znaki, a nie segmenty ścieżki, więc location /app pasuje także do /application.
Rozwiązanie: dopisać końcowy ukośnik i dodać location = /app, jeśli sama ścieżka też wymaga obsługi.
Szczegółowe wyrażenie regularne umieszczone pod ogólnym
Objaw: precyzyjna reguła nigdy się nie uruchamia i nigdzie nie pojawia się błąd. Przyczyna: wyrażenia regularne sprawdzane są w kolejności z pliku, a pierwsze dopasowanie kończy wyszukiwanie, więc wszystko pod szerokim wzorcem jest nieosiągalne. Rozwiązanie: przenieść szczegółowy wzorzec nad ogólny albo zawęzić ogólny kotwicą.
Wyrażenie regularne zapisane po ^~
Objaw: reguła deny wczytuje się bez zarzutu i niczego nie blokuje.
Przyczyna: ^~ przyjmuje dosłowny prefiks, a nie wzorzec. nginx nigdy nie protestuje; blok po prostu nigdy nie pasuje do żadnego URI.
Rozwiązanie: użyć modyfikatora wyrażenia regularnego.
# Broken: matches nothing, loads without error
location ^~ "\.php$" { deny all; }
# Fixed
location ~ \.php$ { deny all; }
Założenie, że query string bierze udział
Objaw: blok location zawierający ? nigdy nie pasuje.
Przyczyna: query string jest odcinany podczas normalizacji, a wybór bloku location przebiega wyłącznie na ścieżce.
Rozwiązanie: dopasować ścieżkę i sprawdzić $arg_name wewnątrz bloku.
location /search {
if ($arg_q = "") { return 400; }
}
Debugowanie: jak ustalić, który blok naprawdę wygrał
Techniki są trzy, od najbardziej rozstrzygającej do najtańszej.
Log debugowania odpowiada wprost, ale wymaga binarki zbudowanej ze wsparciem dla debugowania, więc najpierw sprawdza się to:
nginx -V 2>&1 | grep -o with-debug
Potem włącza się go i szuka linii, która nazywa wybrany blok:
error_log /var/log/nginx/debug.log debug;
grep "using configuration" /var/log/nginx/debug.log
Sondy przez nagłówki odpowiedzi są szybsze i nie wymagają builda z debugowaniem. Oznacza się każdego kandydata i odczytuje nagłówki z powrotem:
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 ma znaczenie: bez tej flagi curl usłużnie rozwiązuje .. na własną rękę i testowane jest inne URI, niż zamierzono. Przy składaniu czegoś bardziej rozbudowanego generator poleceń cURL napisze flagi samodzielnie, a ściągawka curl obejmuje resztę. Kiedy sonda wraca jako przekierowanie albo 404 zamiast oczekiwanego nagłówka, zestawienie kodów statusu HTTP zwykle podpowie, który moduł go wyprodukował.
nginx -T wypisuje w pełni scaloną konfigurację, wraz ze wszystkimi plikami include. W ten sposób da się ustalić, w jakiej kolejności naprawdę stoją wyrażenia regularne po złożeniu sześciu plików — rzadko jest to kolejność z pliku, który się właśnie edytowało.
nginx -T | grep -n "location"
Awarię można też odtworzyć w testerze bloków location, zanim ruszy się serwer. Iterowanie na konfiguracji, która nie została wdrożona, jest szybsze niż cykl przeładowań, a tabela decyzyjna podaje etap, na którym odpadł każdy blok.
Zagnieżdżone bloki location, try_files i to, czego nie zmieniają
Zagnieżdżone bloki location uruchamiają ten sam algorytm poziom niżej. Gdy wygra prefiksowy blok location, nginx schodzi do jego dzieci i powtarza tam wyszukiwanie, co oznacza, że zagnieżdżony regex jest sprawdzany przed wyrażeniami z poziomu nadrzędnego:
server {
location ~ \.php$ { }
location /a/ {
location ~ \.php$ { return 200 "nested\n"; }
}
}
/a/x.php obsługuje zagnieżdżony blok. Zagnieżdżanie ma też mniej oczywisty skutek: potrafi uczynić globalnie dłuższy prefiks nieosiągalnym, bo na każdym poziomie schodzi się tylko do zwycięzcy. Jeśli location /a/bb/ jest zagnieżdżone w location /a/, a rodzeństwo location /a/b stoi na poziomie zewnętrznym, żądanie /a/bb/x trafi do /a/b. Porównanie zewnętrzne odbywa się pierwsze i wygrywa je /a/b. Zejście, które niczego nie znalazło, również się nie cofa; żądanie zostaje przy rodzicu.
try_files i rewrite nie są częścią wyboru. Wykonują się wewnątrz bloku, który już wygrał, i nie mogą sięgnąć wstecz, żeby to zmienić. Jeśli żądanie nigdy nie dociera do bloku z try_files, dyrektywa nie ma znaczenia, a zwykłym winowajcą jest regex ~ \.php$, który przejmuje żądanie, zanim blok prefiksowy w ogóle dostanie swoją kolej. Jest jeden wyjątek: przekierowanie wewnętrzne (rewrite … last albo skok przez error_page) restartuje dopasowanie, więc przepisane URI jest ponownie rozstrzygane wobec listy bloków location od początku.
301, które przychodzi przy żądaniu katalogu bez końcowego ukośnika, też nie jest błędem dopasowania. Produkują je dwa osobne mechanizmy. Jeśli blok location, którego nazwa kończy się na /, niesie proxy_pass albo inną dyrektywę *_pass, żądanie tej samej ścieżki bez ukośnika dostaje 301 już podczas wyboru, przed ewaluacją jakiegokolwiek wyrażenia regularnego i z zachowanym query stringiem:
server {
location /user/ { proxy_pass http://backend/; }
}
# GET /user?x=1 -> 301 to /user/?x=1
Dodanie location = /user tłumi to przekierowanie. Osobno moduł plików statycznych wystawia własne 301, gdy ścieżka rozwiązuje się do prawdziwego katalogu na dysku, co zależy od systemu plików, a nie od konfiguracji.
FAQ
Jakie jest pięć modyfikatorów location w nginx?
Pięć modyfikatorów location w nginx to = dla dopasowania dokładnego, ^~ dla prefiksu pomijającego fazę regex, brak modyfikatora dla zwykłego prefiksu, ~ dla wyrażenia regularnego z rozróżnianiem wielkości liter oraz ~* dla wersji bez rozróżniania. Szósta forma, location @name, nigdy nie bierze udziału w dopasowywaniu URI i istnieje wyłącznie jako cel dla error_page i try_files.
Czy dokładne dopasowanie = przyspiesza nginx?
Blok location z dopasowaniem dokładnym kończy wyszukiwanie natychmiast: nginx nie skanuje prefiksów i nie ewaluuje wyrażeń regularnych. Oszczędność jest realna, ale zbyt mała, żeby dało się ją zauważyć. Ma sens przy endpointach odpytywanych tysiące razy na sekundę, takich jak health checki czy /favicon.ico. Stos bloków = dla zwykłych podstron kosztuje więcej w złożoności konfiguracji, niż daje.
Czy można napisać modyfikator location bez spacji, na przykład ~*^/api?
Tak. location ~*^/api/ i location ~* ^/api/ znaczą dokładnie to samo, bo nginx odcina modyfikator z początku nazwy i dopasowuje najpierw najdłuższy modyfikator, więc ~* rozpoznawane jest przed ~. Mimo to lepiej spację zostawić. Sklejony modyfikator czyta się jak część wzorca i bywa błędnie parsowany przez ludzi podczas review.
Czym różni się root od alias wewnątrz bloku location?
root dokleja całe URI do katalogu, a alias zastępuje nim dopasowany prefiks. Przy location /static/ { root /var/www; } żądanie /static/x.css szuka /var/www/static/x.css; po zmianie na alias /var/www/assets/; szuka /var/www/assets/x.css. Przy alias albo i blok location, i ścieżka dostają końcowy ukośnik, albo nie dostaje go żadne z nich.
Czy jedno żądanie może pasować do więcej niż jednego bloku location?
Pasować może kilka bloków, ale obsługuje żądanie dokładnie jeden. nginx porównuje każdy prefiksowy blok location i, gdy trzeba, każdy blok location z regex, po czym przekazuje żądanie jedynemu zwycięzcy. Dyrektywy nie są dziedziczone z przegranych bloków. Wszystko, co ma obowiązywać wszędzie, musi stać na poziomie server lub http albo zostać powtórzone.
Czy nginx -t powie, który blok location zostanie dopasowany?
Nie. nginx -t sprawdza składnię i poprawność konfiguracji; nigdy nie symuluje żądania, więc o kolejności dopasowania nie mówi nic. Żeby ustalić, do którego bloku należy dane URI, trzeba przeczytać log debugowania, dodać tymczasowy nagłówek odpowiedzi albo wkleić konfigurację do testera bloków location i odczytać, dlaczego każdy blok wygrał lub przegrał.