ByteScope

301

How to fix HTTP 301 Moved Permanently

3xx redirectionIETF standardRFC 9110

The old URL is retired and the one in `Location` replaces it — and because a 301 is one of the few responses a cache may store without being told to, a 301 sent by mistake is the hardest redirect to take back.

Why you are seeing 301, and what to do about it

The same three digits are three different problems depending on which side of the request you are on. Read the block that describes you.

Reproduce 301 with curl

Run this against the URL that failed. It prints the status without the error page, so you can see what the server said rather than what the browser rendered.

curl
curl -sS -D - -o /dev/null -L -w '\n%{num_redirects} hops, final %{http_code} at %{url_effective}\n' http://example.com/old-page

`-D -` dumps the response headers of every hop to stdout rather than only the last, `-o /dev/null` throws each body away, `-L` follows the chain, and `-w` prints how many hops were taken, the final status and the URL that produced it. Read it top down. Each status line tells you whether that hop was a 301 or a 302, which decides whether a cache is allowed to keep it. Each `location:` tells you the shape of the target — an absolute `http://` on a site that then redirects back to HTTPS is a loop; an internal hostname or a stray `:8080` is a proxy that told the application the wrong thing about the request. And on the 301 itself, look for `cache-control` or `expires`: if neither is there, the response is still heuristically cacheable under RFC 9110 §15.1, and that is the version of the redirect your visitors will keep. Two more moves make this decisive. curl holds no cache, so if the browser redirects and this command does not, the redirect only exists on your machine. And curl documents that it downgrades POST to GET when following a 301, 302 or 303 — so running the same command with `--post301` and comparing what the server logs is how you prove whether the redirect is what is eating your request body.

What 301 actually means

RFC 9110 §15.4.2 defines 301 as the target resource having been assigned a new permanent URI, with the server generating a `Location` field holding the preferred one, and it explicitly allows a user agent to rewrite references it holds for the old URI. Three properties of that definition are what people actually trip over. First, permanence is a promise to caches, not a note in your config: RFC 9110 §15.1 lists 301 among the status codes that are heuristically cacheable, so a 301 carrying no `Cache-Control` at all may still be stored and reused — by a CDN, by a proxy and by the browser itself — and reused without asking you again. Second, the method is not guaranteed to survive: §15.4.2 says that for historical reasons a user agent MAY change the request method from POST to GET on the subsequent request, and names 308 (RFC 7538) as the code to use when that behaviour is undesired. Third, `Location` is a URI-reference and RFC 9110 §10.2.2 allows it to be relative, resolved against the target URI — so a redirect that looks fine in your config can still land on the wrong scheme, host or port once whatever generated it has been told the wrong thing about the request it is answering. Nothing in the specification gives you a way to withdraw a 301 that has already been stored somewhere else, which is the single fact that should decide whether you ship one today.

Codes 301 gets mistaken for

Confused withHow to tell them apart
308308 (RFC 7538) is the permanent redirect that guarantees the method and body are replayed unchanged, which is exactly the guarantee RFC 9110 §15.4.2 withholds from 301 when it permits a user agent to turn a POST into a GET. Both are cacheable by default, so 308 is every bit as sticky — the choice between them is about the method, not about how long the redirect lives.
302Permanence is the visible difference and cacheability is the one that costs money. 301 is in RFC 9110 §15.1's list of heuristically cacheable status codes; 302 is not, so a 302 with no explicit `Cache-Control` is not stored and can be changed freely on the next request. That is the whole argument for redirecting with 302 until you are sure, then promoting.
HSTSNot every jump from `http://` to `https://` is a redirect from your server. Once a host has sent `Strict-Transport-Security`, the browser rewrites the URL before any request leaves the machine — Chrome's network panel shows this as `307 Internal Redirect` with `Non-Authoritative-Reason: HSTS`. It has no server-side counterpart to delete, so hunting your config for the redirect that causes it finds nothing.
CloudflareA redirect can be issued at the edge before your origin is consulted at all — Cloudflare's Always Use HTTPS, Bulk Redirects and redirect Rules all produce one — so a 301 that is nowhere in your server config can still be real. The matching trap is the other direction: Cloudflare documents that Flexible encryption plus an origin that redirects HTTP to HTTPS is an infinite loop, because the origin never sees the HTTPS it is asking for.
Reason phrase
Moved Permanently
Class
3xx redirection
Defined by
RFC 9110
Standard
IETF standard

Tools on this site

All of these run in your browser — nothing is uploaded.

Related status codes

Codes people usually end up reading in the same session as 301.

Frequently asked questions

How do I find out which server produced this status code?

Read the response headers rather than the page. curl -sS -o /dev/null -D - https://example.com/path prints them without the body, and server, via and cf-ray between them name the layer that answered. A cf-ray value means Cloudflare handled the response and is the id their support will ask for. To take the edge out of the picture entirely, repeat the request with --resolve example.com:443:203.0.113.10, which connects to the origin address you name while still sending the original host name and SNI — if the answer changes, the edge and the origin disagree.

Does the reason phrase after the number matter?

No, and you should never branch on it. RFC 9112 tells clients to ignore the reason phrase, servers are free to change it, and HTTP/2 and HTTP/3 do not carry one at all — so 404 Not Found over HTTP/1.1 arrives as a bare 404 over HTTP/2. The phrases on this site are the registered ones because they are what people search for and what appears in an HTTP/1.1 log, not because any software depends on them.

Should I retry a request that failed with this code?

Retry 5xx and 429; do not retry 4xx, because nothing about the request will be different next time. If the response carries Retry-After, honour it — RFC 9110 defines it for exactly this, and it may be either a number of seconds or a date. Otherwise use exponential backoff with jitter and a hard cap, and only for idempotent methods: a retried POST can charge a card twice. A retry storm against a server that is already failing is how a brief incident becomes a long one.

Can I make my server return a different status code?

Yes, and the only rule that matters is that the code has to be true. Anything that reads your responses automatically — search engines, monitoring, caches, client retry logic — makes decisions from the number and never from the page. Serving an error page with 200 hides the failure from your own alerting; serving 200 for a missing page gets the error indexed as content; returning 500 for a request that was simply malformed sends whoever is on call to the wrong half of the stack.

Still stuck on 301? browse every status code in the reference — or go back up to the block written for your side of the stack.