How to check redirect chain
Follow the hops and count them in one command: curl -sIL -o /dev/null -w '%{http_code} %{num_redirects} %{url_effective}\n' https://example.com/. The three values are the final status, the number of hops taken, and the URL the chain ended on. Expect 0 or 1 hops. Every hop past the first is a round trip a user pays for.
Checker offline. Follow the manual steps below, they give the same answer.
Why check this
Run this after a domain move, a CMS migration or any change to rewrite rules, and on every release that touches routing. Each hop is a full request and response before the real page starts loading, with DNS, TCP and TLS on top when the hop changes host or scheme. A chain that grew from one hop to three adds two of those round trips. A chain that drops from HTTPS to HTTP sends the next request, cookies included, in clear text.
Prerequisites
- curl 7.x or later.
-Land the write-out variables used here exist in every build. - The curl -L documentation and the status code meanings in RFC 9110.
- Node 22 for two local targets, so the loop and credential tests never run against someone else's host. Save the loop target as
loop-target.jsand runnode loop-target.jsin another shell before step 5.
const http = require('node:http');
http
.createServer((req, res) => {
if (req.url === '/a') res.writeHead(302, { location: '/b' });
else if (req.url === '/b') res.writeHead(302, { location: '/a' });
else res.writeHead(200, { 'content-type': 'text/plain' });
res.end();
})
.listen(8081, '127.0.0.1', () => console.log('loop target on http://127.0.0.1:8081'));
- Save the credential target as
auth-target.jsand runnode auth-target.jsbefore step 7.
const http = require('node:http');
// localhost is a second hostname for the same 127.0.0.1 socket.
const hops = {
'/same': 'http://127.0.0.1:8941/end',
'/port': 'http://127.0.0.1:8942/end',
'/host': 'http://localhost:8942/end',
};
for (const port of [8941, 8942]) {
http
.createServer((req, res) => {
if (hops[req.url]) {
res.writeHead(302, { location: hops[req.url] });
return res.end();
}
const auth = req.headers.authorization ? 'present' : 'absent';
res.end(`port=${port} host=${req.headers.host} authorization=${auth}\n`);
})
.listen(port, '127.0.0.1', () => console.log(`listening on 127.0.0.1:${port}`));
}
Steps
- Step 1.
Read the first hop on its own, before following anything.
curl -sI https://httpbin.org/redirect/3 | grep -i -E '^HTTP/|^location'HTTP/2 302 location: /relative-redirect/2Without
-Lcurl stops here. This is the response your CDN and your monitoring see first. - Step 2.
Follow the chain and print every hop in order.
curl -sIL https://httpbin.org/redirect/3 | grep -i -E '^HTTP/|^location'HTTP/2 302 location: /relative-redirect/2 HTTP/2 302 location: /relative-redirect/1 HTTP/2 302 location: /get HTTP/2 200Three
302responses, then the200. Read the pairs top to bottom: each status belongs with thelocationunder it. - Step 3.
Reduce the same chain to the three numbers a test can assert on.
curl -sIL -o /dev/null -w 'code=%{http_code} hops=%{num_redirects} final=%{url_effective}\n' https://httpbin.org/redirect/3code=200 hops=3 final=https://httpbin.org/get%{http_code}is the last status, not the first.%{num_redirects}is the hop count, and it is the value to pin in a regression test. - Step 4.
Add
%{num_connects}and watch for a scheme change hiding in the chain.curl -sL -o /dev/null -w 'code=%{http_code} hops=%{num_redirects} conns=%{num_connects} final=%{url_effective}\n' https://httpbin.org/absolute-redirect/2code=200 hops=2 conns=2 final=http://httpbin.org/getThe request started on
httpsand ended onhttp. Two connections were opened because the second scheme needs its own. A final URL that does not beginhttpsis a defect whatever the status says. - Step 5.
Point curl at the loop target with a hop ceiling.
curl -sL --max-redirs 10 -o /dev/null -w 'hops=%{num_redirects} final=%{url_effective}\n' http://127.0.0.1:8081/a; echo "exit=$?"hops=10 final=http://127.0.0.1:8081/a exit=47Exit 47 is "too many redirects". The final URL equals the starting URL, which is the signature of a loop rather than a long chain. Stop the Node process afterwards.
- Step 6.
Print the hosts in a chain that leaves your domain.
curl -sIL -w 'hops=%{num_redirects} conns=%{num_connects} final=%{url_effective}\n' 'https://httpbin.org/redirect-to?url=https://example.com/' | grep -i -E '^HTTP/|^location|^hops'HTTP/2 302 location: https://example.com/ HTTP/2 200 hops=1 conns=2 final=https://example.com/conns=2: the200came over a new connection toexample.com, not from the host under test. - Step 7.
Send credentials through each hop of the credential target with both flags.
for f in -L --location-trusted; do echo "$f"; for p in same port host; do curl -s -u user:pass "$f" http://127.0.0.1:8941/$p; done; done-L port=8941 host=127.0.0.1:8941 authorization=present port=8942 host=127.0.0.1:8942 authorization=absent port=8942 host=localhost:8942 authorization=absent --location-trusted port=8941 host=127.0.0.1:8941 authorization=present port=8942 host=127.0.0.1:8942 authorization=present port=8942 host=localhost:8942 authorization=presentWith
-Lonly the same-origin hop keptAuthorization. curl withholds it from other origins, and a port change counts.--location-trustedsends it to every host. - Step 8.
Repeat with Node 22
fetch.node --input-type=module -e "for (const p of ['same', 'port', 'host']) { const r = await fetch('http://127.0.0.1:8941/' + p, { headers: { authorization: 'Bearer t0k3n' } }); process.stdout.write(p + ': ' + (await r.text())); }"same: port=8941 host=127.0.0.1:8941 authorization=present port: port=8942 host=127.0.0.1:8942 authorization=absent host: port=8942 host=localhost:8942 authorization=absentThe same two hops lost it. Stop the Node process afterwards.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| hops=0 and code=200 | No redirect happened | Correct for a canonical URL. Wrong if the test expected a redirect. |
| hops=1 and a final https:// URL | One clean hop | Nothing. This is the target shape for every entry point. |
| hops=2 or more on one hostname | Rules are stacked, usually www plus trailing slash plus locale | Collapse them into one rule. Each hop is a full round trip. |
| final=http://... | The chain downgraded the scheme | Fix before release. The next request travels in clear text. |
| location names another host and conns rises by one | The chain left the domain you requested | Confirm the new host is expected. Authorization stops at this hop. |
| authorization=absent after a port or host change | The client dropped credentials on a cross-origin hop | Call the final URL with credentials. --location-trusted sends them to every host in the chain. |
| exit=47 with the final URL equal to the first | A redirect loop | Look for two rules that each rewrite the other's output. |
| code=302 while the browser shows the page | curl stopped at the first hop | Add -L. Without it the status is the first hop, not the outcome. |
Common mistakes
Thresholds
--max-redirs defaults to 50 and was set to 10 here. Curl stops and exits 47 at the ceiling. Set the ceiling to the number of hops you expect plus one, so a new hop fails the test instead of being followed silently.
What to check next
- How to check if a redirect is 301 or 302: the status on each hop, which the count does not show.
- How to check HTTP to https redirect: the specific chain that must never end on a plain HTTP URL.
- How to check TTFB: what each extra hop costs in time.
- How to check HTTP status code: reading a status without letting curl follow it first.
- Redirect chain checker: the same walk from a server, with each hop listed.
FAQ
How do I check a redirect with curl?
curl -sI URL shows the first hop and curl -sIL URL the whole chain. Add -w '%{num_redirects} %{url_effective}\n' for a value to assert on, not lines to read.
How do I check the number of redirect hops?
Print %{num_redirects} with -L, as in step 3. It counts hops followed, so a loop reports the --max-redirs ceiling, not infinity.
How do I check a redirect in Chrome?
Open DevTools, Network tab, tick Preserve log, then load the URL. Each hop appears as its own row with status 301 or 302. Click a row and read the Location value in the Headers panel.
How do I check a cross domain redirect?
Compare each location host with the one you requested, as in step 6; conns rises by one for a new host. After that hop the client stops sending Authorization, on a port change too, in curl and Node 22 fetch alike. Call the final URL with credentials instead.
Does curl follow redirects by default?
No. Without -L it prints the 3xx response and stops. That default is why a test can report a 301 as a failure when the chain behind it is healthy.
Is a 302 in a chain a problem?
Only if the destination is permanent. A 302 tells caches and search engines to keep asking the old URL, so a permanent move served as 302 keeps paying for the hop forever.
Verified
Verified by Maks Vernycurl 8.21.0node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
- Checker: redirect-chain hops, status codes, final URL, mixed http/https
- Caching and CDN review
- Web performance checklist
- Website launch checklist
- All performance and delivery checks
basic9 minpublished updated Maks Verny