Магия location: приоритеты и модификаторы

location выбирается НЕ по порядку в файле, а по строгому алгоритму приоритетов. Незнание этого — источник самых злых багов конфигурации.
«Длиннейший префикс побеждает независимо от позиции. Регэкспы — в порядке файла. Запомни это — и location перестанет быть магией.»

Внутри server обычно несколько блоков location. Когда приходит запрос, Nginx должен выбрать ровно один. Делает он это по чёткому алгоритму, а не «сверху вниз», как многие думают.

Модификаторы location

  • location = /pathточное совпадение. Высший приоритет, мгновенный выбор.
  • location ^~ /pathпрефикс с приоритетом: если совпал, регэкспы дальше не проверяются.
  • location ~ /patternрегулярка (с учётом регистра).
  • location ~* /pattern — регулярка без учёта регистра.
  • location /path — обычный префикс (без модификатора).

Алгоритм выбора

   Запрос URI
       |
       v
   1. Есть точный  "= URI" ?  --да--> ВЗЯТЬ ЕГО, СТОП
       | нет
       v
   2. Найти ДЛИННЕЙШИЙ префикс среди обычных и ^~
       |
       +-- он с "^~" ? --да--> ВЗЯТЬ ЕГО, СТОП
       | нет (запомнить как запасной)
       v
   3. Проверять регэкспы ~ / ~* В ПОРЯДКЕ ФАЙЛА
       |
       +-- первый совпавший --> ВЗЯТЬ ЕГО, СТОП
       | ни один не совпал
       v
   4. Взять запомненный длиннейший префикс из шага 2

Ключевые мысли: точное — первым; префиксы сравниваются по длине (длиннейший выигрывает) независимо от позиции в файле; регэкспы проверяются по порядку и первый совпавший побеждает. ^~ отменяет проверку регэкспов, если префикс совпал.

Смоделируем матчинг префиксов на Python

Сердце алгоритма — выбор длиннейшего совпавшего префикса. Реализуем именно эту часть:

prefixes = ["/", "/images/", "/images/icons/", "/api/"]

def longest_prefix_match(uri, prefixes):
    best = None
    for p in prefixes:
        if uri.startswith(p) and (best is None or len(p) > len(best)):
            best = p          # длиннее -> приоритетнее
    return best

for uri in ["/images/icons/star.png", "/images/cat.png", "/api/users", "/about"]:
    print(f"{uri:30} -> location {longest_prefix_match(uri, prefixes)}")

Попробуй сам ▶ Заметь: /images/icons/star.png уходит в более длинный /images/icons/, а не в /images/ — хотя оба подходят. Именно так Nginx и рассуждает на шаге 2.

Как работает под капотом

Nginx строит из префиксных location дерево для быстрого поиска длиннейшего совпадения, а регэкспы хранит отдельным списком в порядке объявления. Поэтому добавление сотни префиксов почти не замедляет выбор, а вот десятки регэкспов проверяются линейно — их стоит держать в разумном числе и ставить самые частые выше.

Частые ошибки

  • Думать, что location проверяются сверху вниз. Префиксы — по длине, не по порядку. Это разрушает много «логичных» предположений.
  • Регэксп перехватывает то, что должно было уйти в префикс. Например, ~* \.php$ может перехватить запрос, который ты хотел отдать как статику. Используй ^~ для защиты статики.
  • Слишком общий регэксп вверху списка съедает всё, что под ним.

Best practices

  • Защищай каталоги статики через ^~: location ^~ /static/ { ... } — чтобы регэкспы их не перехватили.
  • Точное совпадение = / для главной страницы экономит проверки.
  • Держи регэкспы минимальными и упорядочивай от частых к редким.

Разбор реального набора location

Чтобы алгоритм отложился в голове, разберём типичный сервер с четырьмя блоками: = / (точное совпадение главной), ^~ /static/ (статика), ~* \.php$ (PHP-скрипты) и / (всё остальное на бэкенд). Запрос на / мгновенно уйдёт в точное совпадение. Запрос на /static/app.css поймает приоритетный префикс ^~ /static/, и регулярка про .php уже не будет проверяться — даже если бы в имени файла мелькало php. Запрос на /index.php не имеет длинного префикса, поэтому дойдёт до регулярки и уйдёт обработчику PHP. А /about провалится сквозь всё и попадёт в запасной /.

Главная ловушка здесь — взаимодействие статики и регулярных выражений. Очень частая ошибка новичков: настроить обработку PHP через location ~ \.php$ и не защитить статику приоритетным префиксом. Тогда хитрый запрос может проскочить в PHP-обработчик и попытаться выполнить файл, который должен был просто отдаться как картинка. Поэтому правило-мнемоника: каталоги статики всегда закрывай через ^~, чтобы регулярки физически не могли их перехватить. Помни и про производительность: префиксы Nginx ищет по дереву очень быстро, а регулярки проверяет линейно одну за другой — десятки regex-локаций замедляют выбор, так что держи их число небольшим и ставь самые частые выше по файлу.

Итоги

Выбор location: точное = первым, затем длиннейший префикс (с ^~ — окончательно), затем регэкспы по порядку файла, и в конце — запасной длиннейший префикс. Порядок в файле важен только для регэкспов. Освоив это, ты избавишься от целого класса загадочных багов. Дальше — оптимизация отдачи статики на полную скорость.

Проверьте себя
1. Как Nginx выбирает между несколькими префиксными location?
AПо порядку в файле, сверху вниз
BВыигрывает самый длинный совпавший префикс, независимо от позиции
CСлучайно
DПо алфавиту
2. Что делает модификатор `^~` у location?
AДелает совпадение регистронезависимым
BПри совпадении префикса отменяет дальнейшую проверку регулярных выражений
CЗапрещает доступ
DВключает регулярное выражение