Статья
HAProxy: петля редиректов и X-Forwarded-*
Через балансировщик страница крутит 302/307 сотнями в секунду, по IP бэкенда всё ок — сломаны X-Forwarded-* до PHP-FPM.
Зашёл на приложение через HAProxy — браузер долго крутит загрузку, в DevTools сыпятся редиректы. По прямому IP бэкенда всё открывается сразу. Оказалось: до PHP не доезжали нормальные X-Forwarded-*, и приложение само себя гоняло по неправильному URL.
Что произошло
Симптомы:
- через HAProxy — задержка 5–10 секунд, в access-логе десятки/сотни
302/307в секунду; - напрямую на Apache — ок;
- схема: клиент → HAProxy (HTTPS) → Apache → PHP-FPM.
Приложение (типичный PHP/Laravel-паттерн) собирает URL для редиректов из:
APP_URLв.env;X-Forwarded-Proto/X-Forwarded-Host/X-Forwarded-Port;- заголовка
Host.
Если в этой цепочке что-то врёт — получается петля.
Как искал
Проверил, что это именно redirect storm, а не медленный бэкенд:
curl -IL --max-redirs 5 https://app.example.com
И смотрел access-лог на поток редиректов:
tail -f /var/log/apache2/ssl_access.log | grep -E ' 302| 307'
Дальше сверил, что PHP вообще видит forwarded-заголовки. Быстрый test-headers.php за HAProxy показал: HTTP_X_FORWARDED_PROTO / HOST / PORT пустые или не те, хотя HAProxy вроде что-то выставлял.
Корневая дыра нашлась в Apache: строки ProxyFCGISetEnvIf для HTTP_X_FORWARDED_* были закомментированы — до PHP-FPM заголовки просто не доходили.
Параллельно в HAProxy порт брался как %[dst_port] — это порт самого HAProxy, а не логический порт клиента, что тоже путало X-Forwarded-Port.
Решение
1. HAProxy: явные X-Forwarded-*
Рабочий вариант, если снаружи всегда HTTPS:
backend B-App
mode http
option forwardfor except 127.0.0.0/8
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Host %[req.hdr(Host)]
http-request set-header X-Forwarded-Port 443
server S-App 10.0.0.10:443 check ssl verify none
Если бывают и HTTP, и HTTPS на фронте:
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Forwarded-Proto http if !{ ssl_fc }
http-request set-header X-Forwarded-Port 443 if { ssl_fc }
http-request set-header X-Forwarded-Port 80 if !{ ssl_fc }
2. Apache: прокинуть заголовки в PHP-FPM
Критичный кусок — без него HAProxy можно править бесконечно:
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost/"
ProxyFCGISetEnvIf "%{HTTP:X-Forwarded-Proto} =https" HTTPS on
ProxyFCGISetEnvIf "%{HTTP:X-Forwarded-Proto} =http" HTTPS off
ProxyFCGISetEnvIf "!%{HTTP:X-Forwarded-Proto}" HTTPS on
ProxyFCGISetEnvIf "%{HTTP:X-Forwarded-Port} ^(.*)$" SERVER_PORT %1
ProxyFCGISetEnvIf "!%{HTTP:X-Forwarded-Port}" SERVER_PORT 443
ProxyFCGISetEnvIf "%{HTTP:X-Forwarded-Proto} ^(.*)$" HTTP_X_FORWARDED_PROTO %1
ProxyFCGISetEnvIf "%{HTTP:X-Forwarded-Host} ^(.*)$" HTTP_X_FORWARDED_HOST %1
ProxyFCGISetEnvIf "%{HTTP:X-Forwarded-Port} ^(.*)$" HTTP_X_FORWARDED_PORT %1
ProxyFCGISetEnvIf "%{HTTP:X-Forwarded-For} ^(.*)$" HTTP_X_FORWARDED_FOR %1
</FilesMatch>
3. APP_URL и Host должны совпадать
В .env:
APP_URL=https://app.example.com
URL должен совпадать с тем, что клиент реально пишет в браузере (Host). Если в Apache ServerName внутренний — добавь ServerAlias на внешний домен.
Итог
Если через reverse proxy крутится redirect loop, а бэкенд напрямую живой — почти всегда врут схема/хост/порт для приложения.
Чеклист:
- HAProxy выставляет
X-Forwarded-Proto/Host/Portявно (не%[dst_port]вслепую) - Apache реально прокидывает их в PHP-FPM через
ProxyFCGISetEnvIf APP_URLсовпадает с внешним URL клиента