HTTP Redirections
HTTP redirections instruct a client to fetch a resource from a different URL. The server includes a Location header in the response, and the client follows the new address automatically. Redirections handle domain migrations, HTTPS enforcement, URL restructuring, and load distribution.
How redirections work
A redirection response carries a 3xx status code and a Location header pointing to the target resource. When a browser or crawler receives this response, a second request goes out to the URL specified in Location.
HTTP/1.1 301 Moved Permanently
Location: https://example.re/new-page
The original response body is typically empty or contains a short HTML document with a link to the new location. Clients following the redirect discard this body and load the target URL.
Permanent redirections
A permanent redirect signals the resource has moved to a new URL for good. Clients and caches store the new URL and stop requesting the old one.
301 Moved Permanently
301 is the most widely used permanent redirect. The response is cacheable by default. Clients are permitted to change the request method from POST to GET when following a 301 redirect, and most browsers do exactly this.
308 Permanent Redirect
308 works like 301 but preserves the original request method. This status code was introduced to fill a gap: there was no permanent redirect guaranteeing method preservation. 308 is now part of the core HTTP semantics. A POST request redirected with 308 remains a POST at the target URL.
Temporary redirections
A temporary redirect signals the resource is available at a different URL for now, but the original URL remains valid. Clients continue to request the original URL in the future.
302 Found
302 is the original temporary redirect. The HTTP/1.0 specification did not clearly define whether clients must preserve the request method. In practice, most clients change POST to GET when following a 302. To remove this ambiguity, 303 and 307 were introduced.
303 See Other
303 tells the client to retrieve the redirect target using GET, regardless of the original request method. A common use case is redirecting after a form submission: the server processes the POST, then responds with 303 pointing to a confirmation page the client fetches with GET.
307 Temporary Redirect
307 works like 302 but guarantees method preservation. A POST stays a POST at the redirect target. 307 exists because clients routinely changed the method on 302 responses, creating a need for an unambiguous temporary redirect.
Permanent vs temporary redirects
The choice between a permanent and a temporary redirect is a statement about the original address, not about how long the redirect stays in place. A permanent redirect says the old address is finished and the new one takes over. A temporary redirect says the old address remains the real home and the detour ends at some point.
Consequences follow from the statement rather than from the code number. Permanent redirects transfer ranking signals to the target and let search engines replace the old address in the index. Temporary redirects keep the original address indexed, so signals stay where they were.
Caching behaves the same way. 301 and 308 responses are cacheable by default, and a client storing one stops asking for the old address at all, which makes an incorrect permanent redirect expensive to undo. 302 and 307 are not cacheable by default, so a change of mind takes effect on the next request.
| Intent | Preserves method | Code |
|---|---|---|
| Permanent | No | 301 |
| Permanent | Yes | 308 |
| Temporary | No | 302 |
| Temporary | Yes | 307 |
Site moves, protocol upgrades, and canonical address changes are permanent. Maintenance pages, A/B tests, geographic routing, and login detours are temporary, because the original address stays in service once the condition passes.
Method preservation matrix
| Status | Type | Method preserved | Cacheable by default |
|---|---|---|---|
| 301 | Permanent | No (POST to GET) | Yes |
| 308 | Permanent | Yes | Yes |
| 302 | Temporary | No (POST to GET) | No |
| 303 | Temporary | No (always GET) | No |
| 307 | Temporary | Yes | No |
The specification and browser behavior differ here, and the gap is worth knowing. HTTP permits a client to keep the original method on 301 and 302, and describes the conversion to GET as historical rather than required. Browsers converged on converting anyway, and the behavior is now mandated for them: a redirected POST becomes a GET with the body discarded and the body-related headers stripped.
In practice 301 and 302 always downgrade a POST in a browser, while non-browser clients following the specification to the letter sometimes keep the method. Depending on either behavior is what 307 and 308 exist to avoid.
Both codes originally preserved the method when defined, and early clients split over whether to honor the rule on POST. Prevailing practice settled on the conversion, and 307 and 308 arrived later to state the preserving intent without ambiguity.
Redirects crossing an origin boundary also lose credentials. Browsers strip the Authorization header the moment a redirect chain reaches a different origin, so an authenticated request following a cross-origin redirect arrives without its credentials.
Other 3xx status codes
300 Multiple Choices
300 indicates multiple representations exist for the requested resource. The server provides a list of options and the client selects the most suitable one. This status code is rarely used in practice.
304 Not Modified
304 is technically a 3xx status code, but does not function as a redirect in the forwarding sense. A 304 response tells the client the cached version of the resource is still current. No Location header is involved. The Conditional-Requests article covers 304 in detail.
305 Use Proxy and 306
Common redirect scenarios
HTTP to HTTPS
Enforcing HTTPS is the most common redirect pattern. The server responds to plaintext HTTP requests with a 301 pointing to the secure version of the same URL.
HTTP/1.1 301 Moved Permanently
Location: https://example.re/page
Combining this redirect with Strict-Transport-Security (HSTS) eliminates subsequent plaintext requests entirely once the browser records the HSTS policy.
Domain consolidation
Organizations consolidate multiple domain names to
a single canonical domain. A 301 from
www.example.re to example.re (or vice versa)
ensures search engines index one version and
link equity concentrates on a single domain.
Path changes and site restructuring
When URL paths change during a site migration, a mapping of old paths to new paths through 301 or 308 redirects prevents broken links and preserves search rankings.
Redirect 301 /old-page https://example.re/new-page
Client-side redirect alternatives
Server-side HTTP redirects are the most reliable approach. When server configuration is not accessible, two client-side methods exist.
Meta refresh
An HTML meta element with http-equiv="refresh"
triggers a client-side redirect. The content
attribute specifies the delay in seconds and the
target URL.
<meta http-equiv="refresh"
content="0; url=https://example.re/">
A zero-second delay acts as an instant redirect. Google treats a zero-second meta refresh as a permanent redirect signal. A nonzero delay is treated as a temporary redirect.
JavaScript redirect
Setting window.location in JavaScript redirects
the browser to a new URL.
<script>
window.location.href = "https://example.re";
</script>
JavaScript redirects depend on the client executing scripts. Search engine crawlers render JavaScript for indexing purposes, but rendering adds latency and is less reliable than server-side redirects. Google treats JavaScript redirects as permanent redirect signals.
Soft 404 pattern
A page returning 200 with content stating the resource moved to a new URL is sometimes called a "soft redirect" or soft 404. This pattern provides no machine-readable redirect signal. Clients and crawlers treat the response as a normal 200. Search engines see the old URL as a live page with thin content rather than a proper redirect.
Redirect precedence
When multiple redirect mechanisms exist on a single page, HTTP-level redirects take highest priority because the client processes the status code before parsing the response body. Meta refresh redirects take second priority. JavaScript redirects execute last, only after the script engine runs.
SEO implications
Search engines treat permanent and temporary redirects differently during indexing.
Permanent redirects (301, 308) send a strong signal to search engines indicating the target URL is the Canonical version. Google transfers indexing signals to the target and replaces the old URL in search results.
Temporary redirects (302, 303, 307) send a weak signal. Search engines keep the original URL in the index and continue crawling the source URL, expecting the redirect to end.
Redirect chains
A redirect chain occurs when URL A redirects to URL B, which redirects to URL C. Each hop adds network latency and consumes crawl budget. Googlebot follows up to 10 redirect hops before abandoning the chain. Best practice is to keep chains under three hops, ideally redirecting directly from the original URL to the final destination.
Redirect loops
A misconfigured server or corrupted client cache creates circular redirects where URL A redirects to URL B and URL B redirects back to URL A. Browsers stop after twenty redirects and report an error, a limit Chrome and Firefox both apply. Servers sometimes catch the loop first and return 500 Internal Server Error.
Search crawlers stop far sooner. Googlebot follows up to ten hops in a chain, and Google recommends staying under five, ideally no more than three. A chain inside the browser limit still costs indexing once the chain passes the crawler limit, leaving the final address unseen.
Chains form by accumulation rather than by design. Separate rules for HTTP to HTTPS, www to naked domain, trailing slashes, and legacy paths each add a hop, and four independent rules produce a four-hop chain for a single request. Collapsing them into one rule pointing at the final address removes the latency and the crawler exposure together.
Common causes include conflicting rewrite rules, CMS misconfiguration, and stale Cookies forcing repeated redirects. A TLS-terminating proxy passing plain HTTP to an origin redirecting all HTTP to HTTPS produces the most persistent version, since each side behaves correctly alone and the loop exists only in combination.
Redirect examples
nginx
The return directive answers with a status code and
a location in one step. Codes 301, 302, 303, 307, and
308 all accept a redirect target.
Whole host moved, path and query preserved
server {
listen 80;
listen [::]:80;
server_name old.example.re;
return 301 https://new.example.re$request_uri;
}
HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name example.re www.example.re;
return 301 https://$host$request_uri;
}
$request_uri carries the original path together
with the query string, which is what keeps a
redirect from discarding parameters.
The rewrite directive handles patterns needing
capture, and answers with 301 through the permanent
flag or 302 through redirect. Those two codes are
the only ones rewrite produces, so a
method-preserving redirect needs return with a
regular expression location.
Method-preserving redirect for an API path
location ~ ^/api/v1/(?<rest>.*)$ {
return 308 /api/v2/$rest$is_args$args;
}
Version history matters on the newer codes. nginx treated 307 as a redirect from 1.1.16 and 1.0.13 onward, and 308 only from 1.13.0. Older builds answer with the status and leave the location unused.
The NGINX blog recommends return over
rewrite wherever both fit. rewrite evaluates a
regular expression on every matching request, while
return 301 states the outcome plainly. Reserve rewrite for
captures and path manipulation beyond the reach of
built-in variables.
Apache
Apache documents mod_alias as the tool for
straightforward redirects and mod_rewrite for
anything needing query string manipulation. Reaching
for mod_rewrite first produces configurations
harder to follow than the task warrants.
Single path, permanent
Redirect permanent "/old-page.html" "/new-page.html"
Temporary redirect
Redirect 302 "/promo" "/campaigns/summer"
Pattern match with capture
RedirectMatch permanent "^/blog/([0-9]{4})/(.*)$" "https://example.re/archive/$1/$2"
The permanent, temp, seeother, and gone
keywords cover 301, 302, 303, and 410. Method
preserving codes take a number instead, since no
keyword exists for either.
Redirect 307 "/api/submit" "https://api.example.re/submit"
Redirect 308 "/api/v1" "https://api.example.re/v2"
Apache answers only with codes the build recognizes, and 308 arrived in 2.4.3. Older builds respond 500 rather than redirecting, which makes the failure look like a configuration error rather than a missing feature.
A Redirect matches complete path segments only, and
appends any trailing path to the target. Query
strings survive the hop.
Request bodies are a different matter, and Apache
documents the discarding of a POST plainly.
The cause sits with the default status rather than
with the directive: Redirect answers 302 unless
told otherwise, and the client converts the
POST to a GET on its own. Naming 307
or 308 explicitly preserves both method and body, so
the discarding is a default worth changing rather
than a limitation to work around.
mod_rewrite covers the rest. The R flag sets the
code and pairs with L in nearly every case, because
R alone hands the rewritten address to the next
rule.
RewriteEngine On
RewriteRule "^/docs/(.*)" "https://docs.example.re/$1" [R=301,L]
Query string handling has three controls. QSA
merges the original query with a new one, QSD
discards the original outright, and a bare question
mark at the end of the substitution clears the
query string.
RewriteRule "^/pages/(.+)" "/page.php?page=$1" [QSA,R=301,L]
RewriteRule "^/search" "/find?" [R=301,L]
HTTP to HTTPS, with virtual host access
<VirtualHost *:80>
ServerName www.example.re
Redirect permanent "/" "https://www.example.re/"
</VirtualHost>
HTTP to HTTPS inside .htaccess
RewriteEngine On
RewriteCond "%{HTTPS}" !=on
RewriteRule "^(.*)$" "https://%{HTTP_HOST}%{REQUEST_URI}" [R=301,L]
REQUEST_URI arrives percent-decoded, and
mod_rewrite re-encodes the output again on an
external redirect. The round trip is where the
trouble starts: the percent sign itself gets escaped
to %25, so encoding already present in the path
comes out doubled, turning %20 into %2520. The
NE flag suppresses the re-encoding for rules
handling already-encoded paths.
Encoded slashes follow a separate rule.
AllowEncodedSlashes defaults to off and answers a
request containing %2F with 404 before any
rewrite rule runs, so such a path never reaches the
redirect. Serving them needs the directive set to
NoDecode.
Rules living in a directory context restart from the
top after L, which turns a redirect into a loop.
The END flag stops the pass outright and avoids
the repeat.
Cloudflare
Two dedicated products cover redirects at the edge, with Snippets available for logic neither expresses. The choice between the two depends on scope. Single Redirects work at the zone level and support wildcard patterns, with regular expressions available through Cloudflare configuration. Bulk Redirects work at the account level across domains and hold static lists rather than patterns. Both need the domain proxied rather than DNS-only.
Rule counts and list capacity vary by Cloudflare account, so check the current limits in the dashboard rather than assuming a fixed number.
Both products accept 301, 302, 307, and 308, and default to 301.
The query string is the trap. Both products discard
the query by default, where Apache passes one through
and nginx keeps one through $request_uri. A
migration moving from a server-level redirect to a
Cloudflare rule silently drops campaign parameters
and session tokens unless the preserve option is
switched on.
The preserve option carries its own surprise. Turning the setting on makes the redirect use the incoming query string and discard any query written into the target, even where the original request carried none. Preserving the incoming query and appending a fixed parameter are mutually exclusive.
"action_parameters": {
"from_value": {
"target_url": { "value": "https://example.re/new" },
"status_code": 301,
"preserve_query_string": true
}
}
Order matters when both products are active. Single Redirects evaluate before Bulk Redirects, so a zone-level rule takes effect ahead of an account-level list covering the same address.
PHP
<?php
header("Location: https://example.re/docs", true, 301);
exit();
?>
The third argument sets the status code. The exit
call matters because output after a header call
continues executing and reaches a client already
told to leave.
See also
- RFC 9110 Section 15.4: Redirection 3xx
- 301
- 302
- 303
- 307
- 308
- 304
- Location
- HSTS
- Conditional-Requests
- Soft 404
- HTTP headers