# теория · шаг 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. Какой путь увидит бэкенд?
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 режется на две части
Совпавшая часть - это ровно та строка, что написана в 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:
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 (блок «только для этих методов») тоже:
server {
listen 80;
location ~ \.php$ {
proxy_pass http://127.0.0.1:8000/x/;
}
}
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 переменная с путём.
Зачем это в работе
Раскладка нескольких сервисов по одному домену - самая частая работа обратного прокси, и в ней обе формы встречаются рядом:
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.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий