1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 1 из 5

Один символ, который меняет путь

Коротко

Смотри не на слеш, а на то, есть ли в proxy_pass путь. Нет пути - URI уходит бэкенду целиком. Есть путь (хоть один слеш) - совпавшая с location часть заменяется на него.

  • location /api/ + proxy_pass http://app:8000; даёт бэкенду /api/cart.
  • Та же строка со слешем на конце даёт /cart.
  • Концы держат парой: location /api и proxy_pass http://app:8000/ дают //cart - совпало /api, остаток /cart приклеился к /.
  • Переменная в адресе вместе с путём (http://$backend/) выбрасывает путь запроса целиком: бэкенд получит /. Без пути (http://$backend) URI уходит как пришёл.

Если различаешь proxy_pass с путём и без и знаешь, что делает переменная в адресе - листай до «Теперь сам».

Сначала ответь сам

Три варианта одной строки, каждый в своём сервере. В каждом запрос GET /api/cart. Какой путь увидит бэкенд?

http server
location /api/ { proxy_pass http://app:8000;    }   # 1
location /api/ { proxy_pass http://app:8000/;   }   # 2
location /api  { proxy_pass http://app:8000/;   }   # 3

app здесь - имя контейнера приложения в Docker, как в первом разделе. Первые два ответа знают все: /api/cart и /cart. Третий - //cart, замер на nginx 1.31.5 ниже. Прежняя версия этого урока утверждала обратное, /cart, и ошиблась на стенде: подставной бэкенд печатал $uri, а в $uri nginx сам склеивает повторные слеши. Путь, который действительно пришёл, виден только в $request_uri.

На что это похоже

Представь стойку в бизнес-центре. Курьер приносит конверт с адресом «третий этаж, кабинет 12». У девушки за стойкой две инструкции на выбор.

Первая: передать конверт дальше как есть. Внутренняя почта сама разберётся, что такое «третий этаж» - там так и написано на дверях.

Вторая: заклеить начало адреса своей наклейкой. На наклейке написано «крыло Б», и после переклейки на конверте останется «крыло Б, кабинет 12». Слова «третий этаж» исчезли: их закрыла наклейка.

Наклейка - это путь в proxy_pass. Пустая наклейка (одинокий слеш) просто стирает начало адреса. Отсутствие наклейки означает первую инструкцию.

Механизм: URI режется на две части

запрос /api/ cart совпало с location остаток, его не трогают пути в proxy_pass нет /api/cart уходит целиком путь есть: /v2/ /v2/ cart путь встал вместо совпавшей части
Совпавшую часть заменяет путь из proxy_pass. Остаток не меняется, пока в адресе нет переменной.

Совпавшая часть - это ровно та строка, что написана в location. Остаток приклеивается к пути буквально, без выравнивания слешей.

Правило

Есть ли в proxy_pass путь - единственный вопрос.

location /api/  +  http://app:8000        →  /api/cart
location /api/  +  http://app:8000/       →  /cart
location /api/  +  http://app:8000/v2/    →  /v2/cart
location /api/  +  http://app:8000/v2     →  /v2cart      ← слеш забыт, слова слиплись
location /api   +  http://app:8000/v2/    →  /v2//cart    ← вот где двойной слеш
location /api   +  http://app:8000/       →  //cart       ← и здесь тоже

Форму выбирают по одному признаку: знает ли сам сервис про префикс. Маршруты внутри приложения начинаются с /api/ - пиши без пути. Префикс придуман нами, чтобы развести сервисы по одному домену, а сервис слушает свои маршруты в корне - пиши с путём.

Разбор: двойной слеш на стенде

Прогон по стенду: четыре блока /b1-/b4 с разными сочетаниями location и proxy_pass, в каждый - запрос /b1/cart, /b2/cart и так далее. Бэкенд - nginx с return 200 "$request_uri\n";, он печатает путь ровно таким, каким тот пришёл:

location proxy_pass бэкенд получил
/b1 http://app:8000/ //cart
/b2 http://app:8000/x/ /x//cart
/b3/ http://app:8000/x/ /x/cart
/b4 http://app:8000/x /x/cart

Все четыре строки - одна арифметика. Совпало /b1, остаток /cart, путь /, на выходе //cart. Совпало /b2, остаток /cart, путь /x/ - /x//cart. Одинокий слеш ничем не особенный.

Если бэкенд печатает $uri, а не $request_uri, в первой строке будет /cart: в $uri повторные слеши склеены. Этим и объясняется ошибка прежней версии урока, и так же легко обмануться при отладке своего сервиса.

Практический вывод: слеши держи парой. location /api/ с http://app:8000/ или location /api без пути в proxy_pass - и никаких //.

Разбор: переменная в адресе выбрасывает путь запроса

Строку с переменной пишут, когда адрес бэкенда должен перерезолвиться на ходу - nginx должен заново спрашивать, какой IP стоит за именем (об этом в разделе про эксплуатацию). resolver - DNS-сервер, у которого он спрашивает; 127.0.0.11 - встроенный DNS Docker, знающий имена контейнеров. Выглядит она как обычная, а ведёт себя иначе. Тот же стенд, запрос /v?/cart?a=1:

/etc/nginx/conf.d/shop.local.confhttp server
resolver 127.0.0.11;
set $backend "app:8000";
строка бэкенд получил
proxy_pass http://$backend; /v1/cart?a=1
proxy_pass http://$backend/; /
proxy_pass http://$backend/x/; /x/
proxy_pass http://$backend$uri; /v4/cart
proxy_pass http://$backend$request_uri; /v5/cart?a=1

Вторая и третья строки не отрезают префикс, а выбрасывают весь путь. Приложение получает корень на каждый запрос: не 404, который заставил бы искать причину, а живой ответ главной страницы на любой адрес.

Правило то же, что без переменной, с одной разницей: если путь в адресе есть, он заменяет весь URI, а не только совпавшую часть. Без пути, как в первой строке, URI уходит целиком. Нужен путь запроса - пиши его руками: $uri теряет строку запроса, $request_uri сохраняет её вместе с вопросительным знаком.

Где правило не действует вовсе

Путь в proxy_pass запрещён там, где нечего вырезать. Регулярка совпадает не префиксом, у именованного блока адреса нет, у if (блок-условие) и limit_except (блок «только для этих методов») тоже:

/etc/nginx/conf.d/shop.local.confhttp
server {
    listen 80;
    location ~ \.php$ {
        proxy_pass http://127.0.0.1:8000/x/;
    }
}
↳  выводnginx -t
nginx: [emerg] "proxy_pass" cannot have URI part in location given by regular expression, or inside predicate location, or inside named location, or inside "if" statement, or inside "limit_except" block in /etc/nginx/conf.d/shop.local.conf:4
nginx: configuration file /etc/nginx/nginx.conf test failed

Ошибка громкая и до прода не доезжает. А вот rewrite ... break в том же блоке тихо отменяет путь (rewrite переписывает URI по регулярке, $1 - первая скобочная группа, break - обрабатывать переписанное здесь же; подробноРазбирается в разделе 13, глава «return против rewrite»: rewrite: флаги и ловушки): URI уже переписан, и бэкенду уходит переписанный целиком. Строка rewrite ^/name/([^/]+) /users?name=$1 break; рядом с proxy_pass http://app:8000/ignored/; отправляет на бэкенд /users?name=vasya, и слово ignored в конфиге не значит ничего.

Что ломается без этого

Симптом всегда один: через прокси 404 на всё, напрямую тот же сервис работает. Пути в конфиге при этом выглядят правильно, потому что смотришь ты на адрес в браузере, а бэкенд видит другой.

Диагностика занимает минуту: сравни строку в логе nginx (там $request_uri - что прислал клиент) со строкой в логе приложения (что дошло до него). Лишний префикс - слеш потерян. Недостающий - слеш лишний. Двойной слеш - концы не в паре. Корень вместо адреса - в proxy_pass переменная с путём.

Зачем это в работе

Раскладка нескольких сервисов по одному домену - самая частая работа обратного прокси, и в ней обе формы встречаются рядом:

/etc/nginx/conf.d/shop.local.confhttp server
location /api/     { proxy_pass http://app:8000;      }   # маршруты приложения начинаются с /api/
location /grafana/ { proxy_pass http://grafana:3000/; }   # grafana живёт в корне, префикс наш

Две соседние строки отличаются одним символом, и это не небрежность, а два разных ответа на вопрос о префиксе. Человек, который «наводит порядок» и приводит их к одному виду, ломает ровно один из двух сервисов.

Теперь сам

Дано: location /adm { proxy_pass http://panel:9000/admin/; }, запрос GET /adm/users?page=2. Что получит бэкенд и как это починить?

Совпало /adm, остаток /users, путь /admin/ - выходит /admin//users?page=2. Строка запроса едет отдельно и не участвует в замене. Чинится добавлением слеша в location: пара /adm/ и /admin/ даёт /admin/users?page=2.

Главное

Вопрос один: есть ли в proxy_pass путь. Нет - URI уходит целиком; есть - совпавшая с location часть заменяется на него, а остаток приклеивается буквально. Поэтому концы держат парой: location /api с http://app:8000/ даёт //cart. Переменная в адресе вместе с путём заменяет весь URI: http://$backend/ шлёт бэкенду корень, и путь надо писать руками через $request_uri.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.