A redirect chain is a redirect that points at another redirect. Browsers follow the whole sequence without showing it, so a chain can sit on a site for years. Here is how to see one with curl, what to look for, and how to collapse it.
What a chain looks like
Take a site that has three sensible rules: force HTTPS, force www, and send an old path to a new one. Each rule is correct. Together they turn one request into three hops (the hostnames below are placeholders):
http://example.com/old-page
-> https://example.com/old-page 301
-> https://www.example.com/old-page 301
-> https://www.example.com/new-page 302
-> 200
Trace it by hand
Ask curl for headers only, and tell it to follow redirects:
curl -sIL http://example.com/old-page
-I sends a HEAD request, -L follows each Location header, and -s hides the progress bar. The output is one block of headers per hop. Two lines in each block matter: the status line and Location.
To cut it down to those lines:
curl -sIL http://example.com/old-page | grep -iE '^(HTTP|location)'
For a one-line summary, use curl's write-out variables:
curl -sIL -o /dev/null -w '%{num_redirects} hops, ends at %{url_effective}\n' http://example.com/old-page
Some servers answer HEAD differently from GET. If the result looks wrong, swap -I for -o /dev/null -D - so curl sends a GET and prints the headers.
What each hop costs
Every hop is a full round trip before the browser can ask for the page it actually wants. When a hop changes the protocol or the hostname, the browser also needs a new connection, which means another DNS lookup and another TLS handshake. On a slow mobile connection that is time the visitor spends looking at nothing.
The 302 is the real problem
Look at the status codes, not only the count. A 301 says the move is permanent. A 302 says it is temporary. One 302 in a chain of 301s makes the whole sequence ambiguous: a crawler cannot be sure the final URL is the permanent home of the content. In the example above, the last hop is the one to fix.
In the curl output it is easy to miss, because it is one status line among several. The grep above puts every status code in a column so the odd one stands out.
Where chains come from
Usually from rules living in more than one place: the CDN, the web server, the application, and a CMS plugin. Each rule was written by someone who could not see the others. Nobody wrote a chain on purpose.
The fix
Point every rule at the final URL so any request arrives in one hop. In Nginx, that means the plain-HTTP server block and the bare-domain block both redirect straight to the canonical origin, and the path redirect names the full final URL:
# Before: each block hands off to the next one
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
# After: every variant goes straight to the canonical origin
server {
listen 80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}
location = /old-page {
return 301 https://www.example.com/new-page;
}
Then run the curl command again against every variant of the URL: http, https, with and without www. Each should report one hop.
Limits of this method
curl follows HTTP redirects only. It will not see a <meta http-equiv="refresh"> tag or a redirect done in JavaScript. A server can also answer curl differently from a browser, based on the user agent or cookies. If the browser ends up somewhere curl does not, that is the reason.
If you would rather not read headers by eye, a redirect checker prints the same hops with their status codes and flags loops. Disclosure: I build that one.
Top comments (0)