Ви змінили «одне маленьке перенаправлення» — і раптом у всіх браузерів загорілося «Too many redirects». Сайт ніби телепортується між HTTP та HTTPS або між www і апексом, поки браузер не здається. Логи Nginx виглядають невинно. Ваш балансувальник наполягає, що він не при чому. І всім треба це виправити до наступної наради.
Це випадок №71: цикл канонічного/HTTPS перенаправлення. Це нудно, часто трапляється і цілком уникнуто — якщо перестати гадати й почати перевіряти, що насправді бачить клієнт, що Nginx думає, що бачить, і що робить ваш upstream-додаток.
Що насправді означає «Too many redirects» в термінах Nginx
Браузери автоматично йдуть за HTTP-перенаправленнями. Вони підуть за багатьма з них — поки не перестануть. Коли ви бачите «Too many redirects», це не моральний вирок. Це цикл: запит A викликає перенаправлення на B; запит B викликає перенаправлення на A (або на C, який зрештою повертається до A). Браузер зупиняється, щоб уникнути нескінченного пінг-понгу.
У світі Nginx цикл зазвичай походить з однієї з таких моделей:
- Цикл схеми: HTTP → HTTPS → HTTP (часто через плутанину з заголовками проксі).
- Цикл канонічного хоста:
example.com→www.example.com→example.com(два правила перенаправлення суперечать одне одному). - Цикл нормалізації шляху:
/app→/app/→/app(обробка слешів у Nginx vs додатку vs upstream). - Цикл порту: перенаправлення містить явний
:443або:80і щось «виправляє» це назад. - Змішані рівні: перенаправлення CDN/LB плюс перенаправлення Nginx плюс перенаправлення додатку.
Суб’єктивна порада: якщо у вас і Nginx, і додаток роблять канонізацію, оберіть одну сторону. Суперечливі перенаправлення — це як суперечливі таблиці джерел правди: всі програють.
Одна операційна реальність: цикли перенаправлень можна діагностувати за кілька хвилин у терміналі з дисципліною. Найшвидше виправлення — не «спробуй інший rewrite». Найшвидше — зібрати ланцюжок перенаправлень, знайти атрибут, що переключається (схема/хост/шлях) і прибрати одне з двох конфліктних правил.
Факти та контекст, які роблять проблему менш таємничою
- HTTP-перенаправлення — давнє явище. Коди статусу 301/302 походять із ранніх специфікацій HTTP; «Moved Permanently» з’явився ще до більшості сучасних стеків.
- 301 став інструментом кешування. Браузери й проміжні компоненти можуть агресивно кешувати 301; відлагодження перетворюється на «я виправив, але мій ноутбук не оновився».
- 307/308 існують не просто так. Раніше 302 змінював POST на GET у деяких клієнтів; 307/308 зберігають семантику методу послідовніше.
- Команда
returnв Nginx безпечніша заrewrite. Старий рушій rewrite потужний, але ним легко створити цикли або випадково продовжувати внутрішні перезаписи. - Канонічні перенаправлення хоста почалися як SEO-гігієна. Пошукові системи карали за дублювання контенту; операційники успадкували біль, коли поверх SEO додали TLS.
- Проксі змінили поняття «HTTPS». Якщо TLS завершується на балансувальнику, Nginx бачить plain HTTP, якщо ви не повідомите йому про
X-Forwarded-Proto. - HSTS підняла ставки. Після увімкнення HSTS клієнти будуть намагатися HTTPS у будь-якому разі; зламане HTTPS-перенаправлення стає помітним для користувачів одразу.
- CDN люблять «допомагати». Багато CDN можуть примусово включати HTTPS або переписувати хости. Це добре, доки й Nginx не робить те ж саме.
Швидкий план діагностики (перший/другий/третій)
Перший: зафіксуйте ланцюг перенаправлень точно так, як його бачить клієнт
- Використайте
curl -I -Lі зафіксуйтеLocation, коди статусів і чи змінюються хост/схема на кожному кроці. - Перевірте, чи цикл чергується між HTTP/HTTPS або між хостами (apex vs www).
- Підтвердіть, чи перенаправлення йде від Nginx чи від чогось вище по ланцюжку, дивлячись на заголовки відповіді (
Server, кастомні заголовки або трасувальний заголовок, який ви додасте).
Другий: перевірте, що Nginx вважає запитом
- Перегляньте конфіг Nginx на предмет
return 301,rewrite, блоківifта дубльованихserver-блоків для одного й того ж імені. - Подивіться access-логи з
$scheme,$hostі$http_x_forwarded_proto(тимчасово додайте debug-формат логів, якщо потрібно). - Якщо за проксі/CDN, підтвердіть, що ви довіряєте forwarded-заголовкам тільки з відомих IP.
Третій: ізолюйте шари
- Обійдіть CDN/LB, якщо можливо (напряму в origin IP з потрібним заголовком
Host). - Тимчасово вимкніть канонічні перенаправлення додатку або встановіть базову URL явно, щоб збігалося з політикою Nginx.
- Перевірте ще раз і зупиніться, коли ланцюг матиме максимум одне перенаправлення (краще — нуль для вже канонічних запитів).
Перефразована ідея (приписують керівникам надійності): «Надія — це не стратегія.»
Ставтеся до перенаправлень так само: перевіряйте, не відчувайте за настроєм.
Анатомія перенаправлення: канонічний хост, схема та шлях
Рішення про канонічний хост (виберіть одне й застосуйте один раз)
Канонічний хост означає, що ви вирішуєте, чи сайт «живе» на example.com або на www.example.com. Обидва варіанти підходять; невизначеність — ні. Застосуйте правило на одному рівні — бажано на краю (Nginx), бо це дешево й послідовно.
Дві суперечливі правила канонічності — класичний цикл:
- Nginx змушує
www. - Додаток змушує апекс (або CDN робить це).
- Результат: нескінченне відбиття.
Рішення про канонічну схему (HTTPS — будьте чесні)
Якщо TLS завершується на Nginx, $scheme реальний. Якщо TLS завершується раніше, ніж Nginx, $scheme бреше (він буде http), якщо ви не передасте йому X-Forwarded-Proto або стандартизований Forwarded. Багато прикладів «HTTPS redirect» в інтернеті припускають, що Nginx бачить TLS. Це припущення породжує випадок №71.
Рішення про канонічний шлях (слеші та індексні файли)
Цикли шляху виникають, коли кілька компонентів по-різному «нормалізують» URL. Nginx може перенаправляти /app на /app/ через try_files або autoindex, у той час як додаток повертає на /app, тому що вважає, що маршрути не мають закінчуватися слешем. Визначте канонічну політику і впровадьте її в одному місці.
Жарт №1 (коротко, по темі): Цикли перенаправлень — це просто тест навантаження, на який ви не виділили бюджету.
Практичні завдання: команди, виводи та рішення
Це не «запусти це і сподівайся». Кожне завдання включає, що шукати і яке рішення воно підказує. Виконуйте їх на Debian 13, але логіка переносима.
Завдання 1: Відтворіть з повним трасуванням перенаправлень
cr0x@server:~$ curl -sS -D- -o /dev/null -L -I http://example.com/
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://example.com/
HTTP/2 301
server: nginx
location: http://example.com/
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://example.com/
Що це означає: Схема чергується HTTPS → HTTP → HTTPS. Це цикл. Якщо ви бачите чергування хостів, у вас боротьба за канонічний хост.
Рішення: Перестаньте крутити шляхи. Перейдіть прямо до логіки схеми/каноніки і перевірте заголовки проксі.
Завдання 2: Показати лише Location заголовки (швидкий підпис циклу)
cr0x@server:~$ curl -sS -I http://example.com/ | sed -n 's/^Location: //p'
https://example.com/
Що це означає: Перший хоп — HTTP → HTTPS. Сам по собі це нормально.
Рішення: Тепер протестуйте HTTPS-ендпоінт, щоб побачити, хто повертає вас назад в HTTP.
Завдання 3: Інспект HTTPS-відповіді без переходу за перенаправленнями
cr0x@server:~$ curl -sS -I https://example.com/ | sed -n '1p;/^Location:/p'
HTTP/2 301
location: http://example.com/
Що це означає: Хтось, хто віддає HTTPS, перенаправляє на HTTP. Тим «хтось» може бути Nginx, додаток або проміжний проксі.
Рішення: Визначте, який рівень видав цю відповідь (заголовки, логи і тести обходу).
Завдання 4: Підтвердіть, який Nginx відповідає (відбиток заголовків)
cr0x@server:~$ curl -sS -I https://example.com/ | grep -iE '^(server:|via:|x-cache:|x-served-by:|cf-|x-amz-)'
server: nginx
Що це означає: Не остаточно, але якщо ви бачите CDN-специфічні заголовки, ви не говорите напряму зі своїм Nginx.
Рішення: Якщо підозрюєте проміжний елемент, обійдіть його далі.
Завдання 5: Обійдіть DNS/CDN і влучте в origin IP з Host заголовком
cr0x@server:~$ curl -sS -I --resolve example.com:443:203.0.113.10 https://example.com/ | sed -n '1p;/^location:/Ip'
HTTP/2 301
location: http://example.com/
Що це означає: Навіть влучаючи в origin, HTTPS перенаправляє на HTTP. Ймовірно, це Nginx або upstream-додаток за Nginx.
Рішення: Перевірте конфіг Nginx та upstream-перенаправлення додатку.
Завдання 6: Здампіть активну конфігурацію Nginx (щоб не гадати, який файл включено)
cr0x@server:~$ sudo nginx -T 2>/dev/null | sed -n '1,80p'
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# configuration file /etc/nginx/nginx.conf:
user www-data;
worker_processes auto;
...
Що це означає: Ви отримали відрендерену конфігурацію разом з include файлами. Це ваші дані істини.
Рішення: Шукай вивід за редирект-правилами й дубльованими server-блоками.
Завдання 7: Знайдіть директиви перенаправлення і підозрілі умови if
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -nE 'return 30[12]|rewrite |if \(|server_name|listen 80|listen 443'
412: listen 80;
417: server_name example.com www.example.com;
420: if ($scheme = http) { return 301 https://$host$request_uri; }
612: listen 443 ssl http2;
618: if ($scheme = https) { return 301 http://$host$request_uri; }
Що це означає: Ви буквально маєте протилежні перенаправлення: HTTP→HTTPS в одному сервері, HTTPS→HTTP в іншому. Ось ваш цикл.
Рішення: Видаліть неправильне HTTPS→HTTP перенаправлення. Замініть усім єдиною канонічною політикою.
Завдання 8: Підтвердіть, куди потрапляють запити (який server block) через access-логи
cr0x@server:~$ sudo tail -n 3 /var/log/nginx/access.log
203.0.113.55 - - [30/Dec/2025:11:32:18 +0000] "GET / HTTP/1.1" 301 169 "-" "curl/8.5.0"
203.0.113.55 - - [30/Dec/2025:11:32:18 +0000] "GET / HTTP/2.0" 301 169 "-" "curl/8.5.0"
203.0.113.55 - - [30/Dec/2025:11:32:19 +0000] "GET / HTTP/1.1" 301 169 "-" "curl/8.5.0"
Що це означає: Той самий клієнт, багаторазові 301. Потрібен додатковий контекст: host, scheme і forwarded proto.
Рішення: Тимчасово додайте debug log_format, що друкує важливі змінні.
Завдання 9: Додайте тимчасовий log_format, щоб показати scheme/host/forwarded proto
cr0x@server:~$ sudo tee /etc/nginx/conf.d/zz-debug-logformat.conf >/dev/null <<'EOF'
log_format diag '$remote_addr host=$host scheme=$scheme '
'xfp=$http_x_forwarded_proto uri=$request_uri '
'status=$status loc=$sent_http_location';
access_log /var/log/nginx/access_diag.log diag;
EOF
Що це означає: Ви створили окремий access-log для діагностики, не чіпаючи основний формат.
Рішення: Перезавантажте Nginx і виконайте один curl-запит; потім прочитайте diag-лог.
Завдання 10: Безпечно перезавантажте Nginx і підтвердіть валідність конфігурації
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
cr0x@server:~$ sudo systemctl reload nginx
Що це означає: Синтаксичних помилок немає; перезавантаження застосовано.
Рішення: Тепер ви можете довіряти diag-логу як відображенню поточної поведінки.
Завдання 11: Згенеруйте один запит і прочитайте діагностичний лог
cr0x@server:~$ curl -sS -I https://example.com/ >/dev/null
cr0x@server:~$ sudo tail -n 1 /var/log/nginx/access_diag.log
203.0.113.55 host=example.com scheme=https xfp= uri=/ status=301 loc=http://example.com/
Що це означає: Nginx бачить scheme=https (отже TLS, ймовірно, на Nginx), але все одно повертає перенаправлення на http://. Це явне правило в конфігу, а не плутанина з проксі.
Рішення: Видаліть будь-яке HTTPS→HTTP перенаправлення. Якщо вам потрібен внутрішній HTTP, тримайте його внутрішнім — не понижуйте зовнішніх клієнтів до HTTP.
Завдання 12: Якщо за проксі, перевірте, як виглядає forwarded proto
cr0x@server:~$ curl -sS -I --resolve example.com:80:203.0.113.10 http://example.com/ -H 'X-Forwarded-Proto: https' | sed -n '1p;/^Location:/p'
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
Що це означає: Коли ви підказуєте Nginx, що оригінальна схема була HTTPS, він обирає HTTPS. Добре: вашу логіку можна зробити proxy-aware.
Рішення: Реалізуйте обробку forwarded-proto правильно й безпечно (довіряйте лише відомим IP проксі).
Завдання 13: Визначте, хто слухає порти 80/443 (уникніть тіньових сервісів)
cr0x@server:~$ sudo ss -ltnp | grep -E ':(80|443)\s'
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1234,fd=6))
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=1234,fd=7))
Що це означає: Nginx — єдиний слухач на 80/443. Якщо б ви бачили щось інше (Apache, дев-сервер), ви б дебажили не той процес.
Рішення: Якщо порти конфліктують, виправте це спочатку. Логіка перенаправлень марна, якщо відповідає не та демона.
Завдання 14: Перегляньте файли віртуальних хостів, увімкнені на Debian
cr0x@server:~$ ls -l /etc/nginx/sites-enabled/
total 0
lrwxrwxrwx 1 root root 34 Dec 30 10:58 example.conf -> ../sites-available/example.conf
Що це означає: У вас один увімкнений сайт. Якщо є кілька файлів з перекриваючим server_name, очікуйте непередбачуваного матчингу.
Рішення: Переконайтеся, що саме один канонічний server block «володіє» кожним іменем хоста.
Завдання 15: Протестуйте вибір сервера Nginx з явними Host заголовками
cr0x@server:~$ curl -sS -I http://203.0.113.10/ -H 'Host: example.com' | sed -n '1p;/^Location:/p'
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
cr0x@server:~$ curl -sS -I http://203.0.113.10/ -H 'Host: www.example.com' | sed -n '1p;/^Location:/p'
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/
Що це означає: Обидва хости перенаправляють себе на HTTPS. Якщо ви хотіли каноналізувати на апекс лише, це неправильно (але не обов’язково цикл).
Рішення: Вирішіть канонічний хост і застосуйте його явно (одне перенаправлення, а не два паралельні світи).
Завдання 16: Перевірте, що додаток не віддає власні перенаправлення схеми/хоста
cr0x@server:~$ curl -sS -I http://127.0.0.1:8080/ | sed -n '1p;/^Location:/p;/^Server:/p'
HTTP/1.1 301 Moved Permanently
Server: gunicorn
Location: https://example.com/
Що це означає: Ваш upstream-додаток перенаправляє на HTTPS самостійно. Це може бути коректним, але тільки якщо він погоджується з Nginx. Якщо Nginx теж перенаправляє (або ще гірше — у зворотному напрямку), виникнуть цикли.
Рішення: Виберіть: канонічні перенаправлення або в Nginx, або в додатку. Потім вимкніть іншу сторону.
Шаблони виправлень, що працюють (правильна конфігурація Nginx)
Шаблон A: TLS завершується на Nginx (найпростіше, найнадійніше)
Це найчистіша конфігурація: клієнти підключаються до Nginx на 443, і Nginx знає істинну схему. Ваші перенаправлення можуть використовувати $scheme безпечно, бо він відображає реальність.
Правила:
- Сервер на порті 80: перенаправляє все на канонічний HTTPS-хост.
- Сервер на порті 443: віддає контент; опційно перенаправляє неканонічні хости на канонічний хост (залишаючись на HTTPS).
- Ніколи не «якщо схема https, то редиректувати на http». Якщо вам потрібен plain HTTP для приватної мережі, зробіть це на іншому імені хоста або слухачі, а не знижуючи публічних користувачів.
cr0x@server:~$ sudo tee /etc/nginx/sites-available/example.conf >/dev/null <<'EOF'
# Canonical policy:
# - canonical host: example.com (no www)
# - canonical scheme: https
# - all HTTP requests redirect to https://example.com$request_uri
# - all HTTPS requests to www redirect to https://example.com$request_uri
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Your normal site config:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
return 301 https://example.com$request_uri;
}
EOF
Чому це працює: рішення про каноніку приймається один раз для кожної неканонічної точки входу. Ви ніколи не перенаправляєте з канонічного → неканонічного.
Шаблон B: TLS завершується upstream (балансувальник/CDN), Nginx бачить лише HTTP
Саме тут люди спотикаються. Nginx бачить $scheme=http, бо LB підключається до Nginx по plain HTTP. Якщо ви написали «if scheme is http redirect to https», ви просто змусили LB→origin-хоп теж перенаправляти. Залежно від того, як LB поводиться, це може створити цикл або принаймні зайві перенаправлення.
Що насправді потрібно: «Якщо клієнт використав HTTP, перенаправити на HTTPS». Цю «клієнтську схему» треба брати з довіреного заголовка.
Робіть по-людськи: довіряйте X-Forwarded-Proto лише з підмереж ваших проксі/балансувальників, і використовуйте змінну, що представляє оригінальну схему клієнта.
cr0x@server:~$ sudo tee /etc/nginx/conf.d/forwarded-proto.conf >/dev/null <<'EOF'
# Trust X-Forwarded-Proto only from known proxies/LBs.
# Replace these with your actual proxy subnets.
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# Derive a client-facing scheme.
map $http_x_forwarded_proto $client_scheme {
default $scheme;
https https;
http http;
}
EOF
Тепер використовуйте $client_scheme в логіці перенаправлень замість $scheme:
cr0x@server:~$ sudo tee /etc/nginx/sites-available/example.conf >/dev/null <<'EOF'
# Canonical policy behind a TLS-terminating proxy:
# - canonical host: example.com
# - canonical scheme: https (as seen by the client)
# - Nginx listens on 80 only (proxy-to-origin), but still enforces canonical policy
# using X-Forwarded-Proto from trusted proxies.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# Redirect non-https clients to https canonical.
if ($client_scheme != "https") {
return 301 https://example.com$request_uri;
}
# Redirect www to apex (still https).
if ($host = "www.example.com") {
return 301 https://example.com$request_uri;
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $client_scheme;
}
}
EOF
Так, я використав if на рівні server. Це одне з небагатьох місць, де це нормально. Уникайте if всередині location для складних переписувань; серверні редиректи прості й передбачувані.
Жорстке правило: якщо ви можете винести канонічні перенаправлення на край (LB/CDN), зробіть це і вимкніть їх в Nginx. Але не розпорошуйте відповідальність по рівнях, якщо вам не подобається екстрені дзвінки.
Шаблон C: Нормалізація шляху — припиніть слеш-пінг-понг
Якщо ваш цикл переключає трейлінг-слеші, потрібно уніфікувати політику. Nginx може її змусити, але якщо ваш додаток теж це робить — оберіть одну сторону.
Звичайний безпечний вибір: «директорії закінчуються слешем, файли — ні». Nginx має власну думку з цього приводу. Якщо роутер вашого додатку ненавидить трейлінг-слеші, вимкніть автоматичні редиректи директорій у Nginx, узгодьте try_files і маршрути, або дайте додатку повністю відповідати за це.
За проксі/CDN: довіряти заголовкам без обману себе
Forwarded-заголовки одночасно необхідні й небезпечні. Необхідні, бо origin не бачить TLS клієнта. Небезпечні, бо будь-який клієнт може відправити X-Forwarded-Proto: https і обдурити наївні конфіги, змусивши генерувати HTTPS-посилання, позначати cookie як secure або пропускати перенаправлення.
Зробіть «довіру» явною
Довіра має бути умовною по IP-джерелу. На Debian 13 Nginx зазвичай постачається з пристойними значеннями за замовчуванням, але він не здогадається про межі вашої мережі. Ви повинні встановити set_real_ip_from на ваші фактичні підмережі проксі.
Знайте, який заголовок ваш проксі відправляє
Багато систем використовують X-Forwarded-Proto. Деякі — стандартизований Forwarded. Деякі ставлять вендор-специфічний заголовок. Суть не в назві; суть у послідовності по всьому ланцюжку.
Уникайте сюрпризу «абсолютного перенаправлення»
Nginx може генерувати абсолютні перенаправлення. Якщо ви випадково витікаєте внутрішні hostnames (наприклад origin.internal) у заголовок Location, користувачі безкоштовно дізнаються карту вашої мережі. Це приз не для вас.
Якщо ви бачите перенаправлення на невірний хост, ймовірно ви використали $host, коли мали на увазі фіксований канонічний домен, або ваш проксі несподівано переписує Host.
Коли це не Nginx: додаток, що воює з вами
Деякі фреймворки перенаправляють на «базовий URL», примушують HTTPS або застосовують правила щодо трейлінг-слешів. Якщо додаток працює за проксі і не розуміє forwarded-заголовків, він може думати, що кожен запит — HTTP, і «підвищувати» його — у той час як Nginx (або проксі) знижує назад, або навпаки.
Три тактичні кроки, які виправляють реальні продакшн-системи:
- Встановіть зовнішню URL додатку явно (base URL / public URL). Багато систем мають одну конфігураційну опцію для цього, і вона запобігає плутанині хоста/схеми.
- Переконайтеся, що додаток враховує forwarded-заголовки лише від довірених IP проксі (та сама ідея, що й для Nginx).
- Вирішіть, хто відповідає за перенаправлення. Якщо додатку потрібні вони для маршрутизації, нехай додаток робить нормалізацію шляхів, а Nginx — лише канонікування схеми/хоста — або навпаки. Просто не дублюйте.
Жарт №2 (коротко, по темі): Якщо два компоненти обидва «запроваджують канонічні URL», вони нарешті погодяться — відразу після завершення інциденту.
Три корпоративні міні-історії з полів перенаправлень
Міні-історія 1: Інцидент через неправильне припущення
Компанія мала акуратну конфігурацію: керований балансувальник завершував TLS, потім форвардив трафік на Nginx на порт 80 у приватній мережі. Хтось додав «просте» правило в Nginx: редирект HTTP→HTTPS. Вони протестували це, прокрутивши origin напряму по HTTP і бачили очікуваний 301. Відправили в продакшн.
У продакшні балансувальник підключався до Nginx по HTTP (як задумано). Nginx бачив $scheme=http для кожного запиту. Тож він перенаправляв кожен запит на HTTPS. Балансувальник сумлінно слідував редиректам у своїх health-check’ах і почав падати, бо не міг погодити TLS з origin, що не говорив TLS. Пул виснажився. Сайт померк, хоч «редирект був правильний».
Виправлення було простим: прибрати схему-редиректи з origin і вимагати HTTPS на балансувальнику. Якщо ж обов’язково потрібно примусове перенаправлення на origin, використовувати X-Forwarded-Proto і довіряти лише підмережі балансувальника. Головний урок: не писати редиректи на основі того, що бачить origin, якщо origin не є TLS-точкою завершення.
Після цього додали перевірку деплою: запускається curl -I і порівнюється ланцюжок перенаправлень для публічної точки та обхідного шляху до origin. Наступного разу pipeline зловив невідповідність перед клієнтами.
Міні-історія 2: Оптимізація, що повернулась бумерангом
Інша організація вирішила зменшити кількість редиректів заради продуктивності. Мета: «жодних редиректів зовсім». Вони прибрали редирект на порті 80 і сконфігурували CDN для канонізації. На папері — чище: менше кругових поїздок, менше навантаження на origin і послідовна поведінка глобально.
Потім вони додали друге правило CDN: нормалізувати www в апекс. Тим часом додаток все ще примушував www через legacy-інтеграції. Цього не було видно в базовому моніторингу, бо сайт завантажувався для деяких користувачів — залежно від стану кешу і введеного хоста.
Гірше було в непостійності. CDN кешував 301 в деяких POP’ах. Деякі користувачі бачили цикл, деякі — ні. Внутрішні команди «не могли відтворити», що корпоративною мовою означає «я спробував один раз і втомився».
Вони відновились, оголосивши одну канонічну політику і реалізувавши її в одному місці: CDN. Потім вимкнули примус додатку до www і встановили base URL додатку на канонічний домен. Продуктивність покращилася і лишилася кращою, бо політика перестала осцилювати по рівнях.
Міні-історія 3: Нудна, але правильна практика, що врятувала
Команда платіжного сервісу мала правило: кожна зміна поведінки на краю вимагала «знімок ланцюга перенаправлень», прикріплений до запиту на зміну. Це було нудно. Люди скаржилися. Але це змушувало вияснити: що відбувається для HTTP апекс, HTTP www, HTTPS апекс, HTTPS www і одного дивного шляху з query string.
Під час рутинного оновлення Debian 13 конфіг Nginx був рефакторений. Новий інженер ненавмисно дублював server_name в двох увімкнених vhost-файлах. Nginx не помилявся; він просто обирав один server block для деяких запитів залежно від пріоритету матчингу. Перенаправлення стали непослідовні.
Оскільки була звичка робити знімки, рев’юер помітив, що HTTPS www перенаправляє на HTTP апекс — явно неправильно — до того, як зміна дійшла до продакшну. Ніяких героїчних дій, жодного інциденту, жодного постмортему. Просто відхилення зміни і виправлення.
Ця практика не виглядала інноваційною. Але була. Найкраща робота в операціях часто схожа на паперову роботу — поки одного дня вона не рятує від пожежі.
Типові помилки (симптом → корінна причина → виправлення)
1) Симптом: HTTP ↔ HTTPS пінг-понг
Корінна причина: TLS завершується на проксі, але origin перенаправляє на основі $scheme або недовіреного forwarded-заголовка. Або лишився явний HTTPS→HTTP редирект з старої міграції.
Виправлення: Застосуйте схему на TLS-точці завершення. Якщо Nginx має її примушувати за проксі, використовуйте відображення $http_x_forwarded_proto в $client_scheme і довіряйте йому лише від проксі-IP.
2) Симптом: www ↔ апекс пінг-понг
Корінна причина: Nginx канонізує на www, додаток — на апекс (або навпаки). Іноді CDN канонізує один шлях, а origin — інший.
Виправлення: Виберіть один канонічний хост і застосуйте його в одному шарі. Вимкніть хост-редиректи в іншому шарі або налаштуйте base URL відповідно.
3) Симптом: тільки деякі користувачі бачать цикл
Корінна причина: Кешовані 301 в браузерах, CDN або корпоративних проксі. Або у вас кілька origin/екземплярів з різними конфігами.
Виправлення: Очистіть CDN-кеші для відповідей з редиректами, якщо потрібно. Тестуйте з чистого клієнта і curl. Переконайтесь у консистентності конфігів між інстансами.
4) Симптом: цикл з’являється лише на конкретному шляху
Корінна причина: Нормалізація трейлінг-слешів відрізняється між Nginx і додатком для того маршруту, або try_files спричиняє директорне перенаправлення, яке додаток відхиляє.
Виправлення: Визначте одну політику шляху. Або дайте додатку це обробляти і уникайте Nginx path-redirects, або реалізуйте явні і послідовні редиректи в Nginx і вимкніть нормалізацію в додатку.
5) Симптом: редиректи ведуть на внутрішній хост або невірний порт
Корінна причина: Неправильне використання $host за проксі, який переписує host, або додаток генерує абсолютні URL на основі внутрішньої адреси слухача. Іноді це робить proxy_redirect «допоміжно».
Виправлення: Використовуйте фіксований канонічний домен у return 301. Налаштуйте upstream, щоб він знав свій публічний URL. Перевірте proxy_redirect.
6) Симптом: браузер продовжує цикл навіть після виправлення конфігу
Корінна причина: Закешований 301 у браузері, або HSTS змушує HTTPS і виявляє іншу проблему перенаправлення, або ви не перезавантажили Nginx (таке трапляється частіше, ніж хочеться зізнатися).
Виправлення: Перевірте з curl з чистого середовища. Підтвердіть перезавантаження і активний конфіг через nginx -T. Якщо HSTS увімкнено, переконайтесь, що HTTPS-ендпоінт правильний перед змінами HTTP.
Контрольні списки / покроковий план
Крок за кроком: безпечне виправлення канонічних + HTTPS циклів
- Запишіть вашу канонічну політику в одному реченні: «Канонічний — https://example.com (без www).» Якщо ви не можете її прописати, ви не зможете її домогтися.
- Зберіть ланцюги перенаправлень для чотирьох точок входу: HTTP апекс, HTTP www, HTTPS апекс, HTTPS www. Зафіксуйте коди статусів і цілі
Location. - Обійдіть проміжні елементи (CDN/LB), щоб побачити, чи origin сам створює цикл.
- Відрендерте активний конфіг Nginx через
nginx -Tі знайдіть всі директиви перенаправлення. - Ліквідуйте суперечності: видаліть будь-яке правило, що перенаправляє з канонічного → неканонічного.
- Визначте, де живе примус схеми: якщо TLS завершується на LB/CDN, застосовуйте HTTPS там, а не в origin — якщо ви не імплементуєте довіру до forwarded-proto.
- Визначте, де живе примус хоста: робіть це на краю (Nginx або CDN) і вимкніть хост-редиректи в додатку або налаштуйте base URL відповідно.
- Безпечно перезавантажте (
nginx -tпотімsystemctl reload), протестуйте через curl, потім браузер. - Видаліть тимчасові діагностичні логи після підтвердження стабільності. Діагностика — добре; постійний шум — ні.
- Додайте регресійний тест: скрипт, що перевіряє ці чотири точки входу, має максимум одне перенаправлення і приводить до канонічного URL.
Операційний чекліст: перед тим, як оголосити перемогу
- Канонічний URL повертає
200(або очікуваний статус додатку), а не301. - Неканонічні URL повертають рівно одне перенаправлення на канонічний.
- Ціль перенаправлення ніколи не понижує HTTPS до HTTP.
- Жодне перенаправлення не вказує на внутрішній хост, приватний IP або несподіваний порт.
- Логи підтверджують правильні змінні
Hostі схему. - Health checks (LB/CDN) не слідують за редиректами, що ламають доступність origin.
Питання та відповіді
1) Чому браузер каже «Too many redirects», а в error-log Nginx тиша?
Бо редиректи не є помилками для Nginx. 301 — нормальна відповідь. Браузер виявляє цикл, після повторних редиректів.
2) Чи використовувати rewrite чи return 301 в Nginx?
Використовуйте return 301 для канонічних хост/схема редиректів. Це зрозуміліше і менш схильне до циклів. rewrite — лише коли дійсно потрібна маніпуляція URI через regex.
3) Мій TLS завершується на load balancer. Чи завжди if ($scheme = http) — це помилка?
Це невірно для визначення, що використав клієнт, бо $scheme відображає хоп LB→origin. Використовуйте довірений forwarded-proto заголовок і мапте його в змінну на кшталт $client_scheme.
4) Чи можна довіряти X-Forwarded-Proto з інтернету?
Ні. Будь-хто може його відправити. Довіряйте тільки з відомих підмереж проксі. Інакше ви дозволяєте потенційним атакувальникам впливати на поведінку, важливу для безпеки.
5) Чому я бачу різну поведінку між curl і браузером?
Браузери кешують 301, можуть застосовувати HSTS і іноді мають кеш DNS або поведінку service worker. Curl зазвичай «свіжий», якщо ви не скриптуєте кеш. Якщо браузер відрізняється, тестуйте в приватному вікні і перевірте статус HSTS.
6) Який код статусу використовувати для HTTP → HTTPS?
Для типових сайтів 301 підходить. Якщо ви перенаправляєте POST і дбаєте про збереження методу, розгляньте 308. Послідовність важливіша за тренди.
7) Я виправив цикл, але тепер мене перенаправляє на невірний хост. Чому?
Ймовірно ви використали $host в редиректі, а вхідний Host-заголовок не той, що ви очікували (проксі переписує, альтернативні домени). Для канонізації краще вказувати фіксований домен у return.
8) Як зупинити цикли через трейлінг-слеші?
Виберіть одну політику і впровадьте її один раз. Якщо додаток хоче «без трейлінг-слешу», вимкніть поведінку Nginx, що додає його, і налаштуйте додаток генерувати посилання послідовно. Якщо Nginx відповідає за це, зробіть редирект явним і переконайтеся, що додаток не редиректить назад.
9) Чи змінює Debian 13 щось у перенаправленнях Nginx?
Фундаментально — ні. Ті ж способи виходу з ладу. Практично: оновлення може реорганізувати включувані конфіги або увімкнути нові site-файли, що підвищує ймовірність дублювання server-блоків і конфліктних редиректів.
Висновок: наступні кроки для запобігання повторенню
Цикли перенаправлень — це не містика. Це суперечливі політики, виконані сумлінно. Ваше завдання — прибрати суперечності.
Наступні кроки, які окупаються негайно:
- Оголосіть канонічний URL (схема + хост) і впровадьте його в одному шарі.
- Зробіть Nginx proxy-aware тільки якщо необхідно, і тільки з довіреними forwarded-заголовками.
- Додайте регресійний тест ланцюга перенаправлень в деплойн: чотири точки входу всередині, один канонічний кінець зовні.
- Тримайте редиректи нудними: використовуйте
return, уникайте хитромудрих rewrite-ів і видаляйте старі міграційні правила, коли вони відпрацювали.
Якщо ви зробите це один раз правильно, ви припините бачити випадок №71 під час оновлень, змін CDN або тієї п’ятничної «невеликої SEO правки», яка якимось чином завжди потрапляє в продакшн.