Debugging Redirect URLs: Check 301 vs 302 Redirects, Chains, Loops and Lost Parameters

A redirect is one response header, `Location`, plus a status code. Problems appear when redirects stack up: `http` to `https`, bare domain to `www`, old path to new path, then a login page. Each hop can drop a query parameter, change a `POST` into a `GET`, or point back to where it started. The fix nearly always starts the same way: list every hop and read its status and `Location`.

See every hop with curl

A browser shows only the final page, and DevTools clears the Network tab on navigation unless "Preserve log" is ticked. curl shows the whole chain.

curl -sIL http://example.com/old-page | grep -iE '^(HTTP|location)'
# HTTP/1.1 301 Moved Permanently
# Location: https://example.com/old-page
# HTTP/2 301
# location: https://www.example.com/old-page
# HTTP/2 301
# location: https://www.example.com/new-page/
# HTTP/2 200

# status, final URL and hop count in one line
curl -sL -o /dev/null -w '%{http_code} %{url_effective} hops=%{num_redirects}\n' http://example.com/old-page

-I sends a HEAD request. Some servers answer HEAD differently from GET, so if the result looks odd, repeat with -L -o /dev/null -D - to issue a real GET and print headers. Add -A 'Mozilla/5.0' if the site redirects bots or unknown clients differently, and -H 'Accept-Language: de' to test language redirects.

The example shows a chain of three redirects where one would do. Every hop adds a round trip, often a new TLS handshake, and on mobile networks that is noticeable.

Choosing the status code

301 Moved Permanently   permanent; clients MAY change POST to GET
302 Found               temporary; clients MAY change POST to GET
303 See Other           "go GET this other URL" — after a form POST
307 Temporary Redirect  temporary; method and body are preserved
308 Permanent Redirect  permanent; method and body are preserved

For page moves and domain changes, use 301 or 308. Google treats both as strong signals to move indexing to the new URL, and treats 302 and 307 as temporary. For APIs, prefer 307 and 308. In practice browsers turn a POST into a GET when following 301 and 302, so a redirected form submission arrives without its body.

Redirects in HTML (<meta http-equiv="refresh">) or JavaScript (location.href = …) are invisible to curl and to many checkers, and they are slower and weaker signals for search engines. If a checker says "200" but your browser moves, look for one of these in the page source.

Redirect loops: ERR_TOO_MANY_REDIRECTS

A loop is two rules that undo each other. Browsers give up after about 20 hops. The common causes are few.

HTTPS behind a proxy or CDN. The load balancer terminates TLS and forwards plain HTTP, and the application sees http, redirects to https, and receives the forwarded request as http again. Fix it by trusting X-Forwarded-Proto in the application (for example Laravel's TrustProxies, Django's SECURE_PROXY_SSL_HEADER or Express's trust proxy), or by setting the CDN to connect to the origin over HTTPS. Cloudflare's "Flexible" SSL mode combined with an origin that forces HTTPS is a well-known source of this loop.

Conflicting host rules: the CDN redirects www to the bare domain and the application redirects the bare domain to www. Trailing slashes: one layer adds / and another removes it. Login redirects: the login page itself requires authentication, or the session cookie is not being set (wrong domain, Secure flag on HTTP, or a blocked third-party cookie), so every request looks logged out.

Clear cookies before retesting, since a stale cookie can cause or hide a loop, and use curl -sIL to see which two URLs alternate.

Chains, lost parameters and relative Location headers

Collapse chains to a single hop: every old URL should redirect straight to its final destination. Googlebot follows up to 10 redirect hops, but each one costs crawl time, and chains grow whenever a site migrates again. After a migration, re-point the old redirects at the newest URLs.

Query strings are often lost at one hop. A rule like return 301 https://example.com/new; drops ?utm_source=… and every other parameter. In nginx, use $request_uri or append $is_args$args. In Apache, mod_rewrite keeps the query string unless the target has its own, where the QSA flag merges them.

# nginx: keep path and query when moving to https + www
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://www.example.com$request_uri;
}

Location may be relative (Location: /login), which RFC 9110 allows, and clients resolve it against the request URL. Problems arise when an application behind a proxy builds an absolute URL from its internal host name, giving Location: http://app-internal:8080/login. Configure the public base URL or trust the forwarded headers.

Redirects your server did not send

Some redirects never reach the network. With HSTS, the browser upgrades http:// to https:// itself, and Chrome shows it as 307 Internal Redirect in DevTools. No server rule can remove it until the max-age expires, and for preloaded domains not even then.

Browsers cache 301 and 308 responses, often without an expiry. After you fix or remove a permanent redirect, your own browser may keep following the old one. Test in a private window or with curl, and add Cache-Control: max-age=3600 to permanent redirects while a migration is still settling.

Service workers can also answer navigation requests with a redirect. Check the Application tab if a redirect appears only for returning visitors.

Open redirects and OAuth redirect URIs

An endpoint such as /go?url=https://anything that redirects wherever it is told is an open redirect. Attackers use your trusted domain to front phishing pages and to steal OAuth codes. Allow only relative paths, or check the target against an allow-list of hosts after parsing the URL. A startsWith check on the string is bypassed by https://example.com.evil.net or //evil.net.

OAuth and OpenID Connect providers compare redirect_uri exactly against the registered value. redirect_uri_mismatch errors come from differences a quick glance misses: http versus https, a trailing slash, www, a port, or different percent-encoding. Decode the redirect_uri parameter from the failing request and compare it character by character with the registered one.

Sources and further reading

RFC 9110, section 15.4, defines 301, 302, 303, 307 and 308 and the Location header. Google Search Central documents how redirects affect indexing and the 10-hop limit. The nginx return and rewrite documentation and Apache's mod_rewrite flags cover query-string handling. RFC 6797 defines HSTS. OWASP's Unvalidated Redirects and Forwards Cheat Sheet covers open redirects.

Frequently asked questions

How do I check where a URL redirects?

Run `curl -sIL <url> | grep -iE '^(HTTP|location)'` to see each hop's status and target, or use a redirect checker. In the browser, enable "Preserve log" in the Network tab so earlier hops are not cleared.

What is the difference between a 301 and a 302 redirect?

301 is permanent: search engines move indexing to the new URL and browsers cache it. 302 is temporary: the old URL stays indexed. For moved pages use 301 or 308; for temporary situations use 302 or 307.

How do I fix ERR_TOO_MANY_REDIRECTS?

Find the two URLs that alternate with `curl -sIL`. The usual causes are HTTPS enforcement behind a proxy that forwards plain HTTP, conflicting `www` or trailing-slash rules, or a login page that itself requires login. Clear cookies and retest.

Why does my redirect lose the query string?

The rule builds the target without the original query. In nginx use `$request_uri` or `$is_args$args`; in Apache use `mod_rewrite` with `QSA` when the target has its own parameters.

Why does Chrome show 307 Internal Redirect?

That is HSTS: the browser upgraded `http` to `https` itself because the site sent `Strict-Transport-Security` earlier or is on the preload list. The server never received the `http` request.

How many redirects are too many?

Aim for one hop from any old URL to its final destination. Googlebot follows up to 10 hops and browsers give up around 20, but each hop adds latency and weakens crawling efficiency.