The difference wasn’t the page. It was one invisible character in the address some visitors were arriving with – and a caching default that turned it into a loop.
What happened?
The report came in the middle of a launch: the new page was throwing ERR_TOO_MANY_REDIRECTS for a portion of visitors. High stakes, and worse – we couldn’t reproduce it. It loaded every single time for us.
So we went to the source. We asked the origin server directly, bypassing every layer in front of it:
curl -I --resolve client.com:443:<origin-ip> https://client.com/en/page/
HTTP/2 200
A clean 200. We tried the www version, the no-trailing-slash version, the plain-HTTP version, a dozen browsers and devices. All 200. The server’s access log agreed: for that URL, it had returned nothing but 200s since the page went live. It had never issued a single redirect.
A page that loops for some visitors, on a server that never redirects it. So where was the redirect coming from?
What we found
The answer was one layer up, at the CDN – Cloudflare – and it came down to a single extra slash.
A small stream of requests was reaching the page with a doubled slash after the language prefix:
/en/page/ <- what everyone types
/en//page/ <- what some links and tools were generating
By default, Cloudflare tidies up doubled slashes before it looks up its cache – so both of these point at the same cached entry. But it forwards the original, messy address to the server unchanged. (Two settings govern this: Normalize incoming URLs is On by default, and Normalize URLs to origin is Off.)
WordPress sees /en//page/, decides the correct address is /en/page/, and answers with a redirect. Harmless on its own. The problem was what that redirect looked like next to the real page:
# the doubled-slash request - a REDIRECT, and cacheable for an hour
GET /en//page/
HTTP/2 301
location: https://client.com/en/page/
cache-control: max-age=3600
cf-edge-cache: cache
# the real page - a 200 that is never cached
GET /en/page/
HTTP/2 200
cache-control: no-store, no-cache, must-revalidate
Cloudflare cached that 301 – and because it had already stripped the doubled slash for its cache key, it filed the redirect under the clean address. From there:
- A visitor requests
/en/page/– perfectly normal. - Cloudflare serves the cached
301 -> /en/page/. - The browser follows it – straight back to the same cached 301, at the same address.
- Loop.
That is why it looked random. Cloudflare’s cache is split by region and device, so only visitors routed through a “poisoned” location hit it. And each entry expired after an hour, then re-poisoned on the next doubled-slash request – so it healed and broke on its own, which is precisely the behaviour that makes a bug impossible to pin down by hitting refresh.
It was never one page, either. Pulling the access log, we found 540 doubled-slash requests across 199 different URLs – including the language homepages. Any of them could quietly loop.
The fix
Cloudflare can’t be told to stop merging slashes in its cache key – that part is fixed behaviour. But it can be told to clean the address on the way to the server, too. One setting does it:
Rules -> Settings -> Normalization -> Normalize URLs to origin: On
With that on, the server receives /en/page/ even when the visitor sent /en//page/. It returns a normal 200, never generates the canonical redirect, and there is nothing cacheable left to poison. We verified it end to end by sending a tagged doubled-slash request and reading what the server actually received:
sent to Cloudflare: /en//page/?check=1
arrived at server: GET /en/page/?check=1 -> 200
Single slash, 200, no redirect. Then we purged the four already-poisoned launch URLs (one per language) and confirmed each served the real page again:
| Language | Before | After |
|---|---|---|
| EN | redirect loop | 200 (cache HIT) |
| ES | redirect loop | 200 (cache HIT) |
| IT | redirect loop | 200 (cache HIT) |
| BR | redirect loop | 200 (cache HIT) |
Two things that cost us time, so they don’t cost you yours:
- Fix the cause before you purge, not after. Purging a poisoned URL clears it for about four minutes – then the next doubled-slash request rebuilds the loop. Turn on normalization first, purge second. Otherwise you’ll watch your fix “work” and then break again while you’re still looking at it.
- Don’t test a CDN cache with a hard reload. A forced reload – or
cache: "reload", or aCache-Control: no-cacheheader – tells Cloudflare to skip its own cache and fetch fresh from the server. You end up measuring the origin, seeing everything work, and concluding the bug isn’t real. To see what the cache is actually serving, request it normally:
// "opaqueredirect" here means a redirect really is sitting in the cache
fetch(url, { redirect: "manual" })
This one sent us chasing the wrong layer for longer than we’d like to admit.
Why intermittent problems deserve a closer look
No amount of refreshing the page would have found this. The bug lived between the visitor and the server, in a layer that answered us correctly every single time we asked it directly. “Works for me” wasn’t a sign the report was wrong – it was the most important clue about where the problem actually was.
Finding it meant treating each layer as a separate suspect – the browser, the CDN, the server – and getting each one to tell the truth on its own. The moment we compared what the server returned (a clean 200) with what visitors received (a redirect), the culprit had nowhere left to hide.
Spot problems early. Fix them fast. Especially the ones only some of your visitors can see – because those are the ones that go unreported the longest.
Running into something only a fraction of your visitors experience? Get in touch with us.