ERR_TOO_MANY_REDIRECTS: the 6 configs that cause redirect loops and how to find them
Someone switched the CDN's SSL mode on a Friday afternoon. The CDN started talking to the origin over plain HTTP, the origin redirected every HTTP request to HTTPS, the CDN followed it over HTTP again, and every page on
Someone switched the CDN's SSL mode on a Friday afternoon. The CDN started talking to the origin over plain HTTP, the origin redirected every HTTP request to HTTPS, the CDN followed it over HTTP again, and every page on the site became ERR_TOO_MANY_REDIRECTS. The fix was one dropdown. Finding it took two hours.
Loops almost never come from one bad rule. They come from two layers that each think they're doing the right thing.
TL;DR
- A loop is A redirecting to B redirecting back to A (sometimes through more steps).
- The usual cause: a proxy/CDN and the origin disagree about scheme, host or trailing slash.
- Trace it with
curl -sIL --max-redirs 10and look for the repeat. - Fix it by deciding which single layer owns each canonical rule.
- My opinion: HTTPS and www canonicalisation should live in exactly one layer. Two layers doing it "for safety" is how loops are born.
Step 1: see the loop
Browsers just give up. curl shows you the path:
curl -sIL --max-redirs 10 https://example.com/ 2>&1 | grep -iE '^(HTTP|location)|maximum'
HTTP/2 301
location: https://www.example.com/
HTTP/2 301
location: https://example.com/
HTTP/2 301
location: https://www.example.com/
...
curl: (47) Maximum (10) redirects followed
Two URLs alternating. That's a host disagreement: one layer wants www, another wants the bare domain. If you want to scan many URLs, the hop-by-hop script in the redirect chains post flags loops automatically.
Cause 1: proxy terminates TLS, origin forces HTTPS
The CDN or load balancer handles HTTPS and talks to the app over HTTP. The app sees http://, redirects to https://, and the cycle repeats.
Fix: make the origin trust the forwarded scheme.
# behind a load balancer that sets X-Forwarded-Proto
if ($http_x_forwarded_proto = "http") {
return 301 https://$host$request_uri;
}
In frameworks, enable "trust proxy" (Express: app.set('trust proxy', 1); Django: SECURE_PROXY_SSL_HEADER) so the app knows the original request was already HTTPS.
Cause 2: CDN "flexible" SSL with an HTTPS-only origin
Same family as cause 1, but caused by a CDN setting. "Flexible" mode means CDN to origin is HTTP. If the origin redirects HTTP to HTTPS, you loop. Switch to full/strict mode with a valid origin certificate.
Cause 3: www rule in two places, pointing opposite ways
The DNS host or CDN has a page rule sending www to the apex. The app config sends the apex to www. Each one undoes the other.
grep -rn "www" /etc/nginx/ | grep -i return
Then check the CDN dashboard and the framework settings. Pick one canonical host and delete the opposite rule everywhere else.
Cause 4: trailing slash fights
The web server adds a trailing slash to directories. The app strips trailing slashes. /docs goes to /docs/, then back to /docs.
Common with static hosts plus an SPA router, or a CMS plus a custom nginx rule. Decide on one style and turn off the other layer's normalisation. Trailing slashes: the small URL detail that creates redirects covers how to choose.
Cause 5: cookie or auth based redirects
/dashboard redirects to /login when there's no session. /login redirects to /dashboard when it thinks there is one, maybe because a stale cookie is present but invalid. Users with that cookie loop forever, everyone else is fine.
These show up as "only some users" reports. Reproduce with the user's cookies:
curl -sIL --max-redirs 5 -H 'Cookie: session=abc123' https://example.com/dashboard
Fix: the login page should validate the session, not just check that the cookie exists.
Cause 6: language or geo redirects
/ redirects to /en/ based on Accept-Language, and /en/ redirects to / because the default language shouldn't have a prefix. Or a geo rule bounces a visitor between country sites. Test with different headers:
for lang in en de fr; do
echo "== $lang"
curl -sIL --max-redirs 5 -H "Accept-Language: $lang" https://example.com/ | grep -i '^location'
done
Find loops before users do
Loops often hide on URLs nobody visits from the homepage: old campaign links, legacy paths, uppercase variants. A crawl of internal links won't hit them. Feed in:
- old sitemaps and analytics landing pages
- URLs from email newsletters, which break in their own ways (see Why links in old email newsletters break)
- uppercase, trailing-slash,
http://and non-wwwvariants of top pages
A small generator for variants:
from urllib.parse import urlsplit, urlunsplit
def variants(url):
s = urlsplit(url)
hosts = {s.netloc, s.netloc.removeprefix("www."), "www." + s.netloc.removeprefix("www.")}
paths = {s.path, s.path.rstrip("/") or "/", s.path if s.path.endswith("/") else s.path + "/"}
for scheme in ("http", "https"):
for h in hosts:
for p in paths:
yield urlunsplit((scheme, h, p, s.query, ""))
Pipe those through a hop tracer and flag anything that repeats or exceeds 5 hops. A link checker like linkbruno.com will report redirect loops it meets during a crawl, but the variant list is what catches the ones no internal page links to.
My take
Every canonical rule (scheme, host, slash, language) needs exactly one owner. Write down which layer owns each one: CDN, load balancer, web server or app. Most loops I've traced came from a second layer "helpfully" adding the same rule after the first one already existed.
Over to you
Where do you put your HTTPS and www redirects: CDN, web server or app? And what's the strangest redirect loop you've had to untangle?
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.