HTTP Redirects
An HTTP redirect is a response with a 3xx status code and a Location header that names another URL. The browser follows it on its own, sends a new request there and shows only the final page. For example, GET http://shop.example.com/sale can return 301 Moved Permanently with Location: https://shop.example.com/offers; the browser then requests that address and receives 200 OK.
Five codes do the everyday work: 301, 302, 303, 307 and 308. They differ on two questions: is the move permanent, and may the client turn a POST into a GET when it follows the redirect?
Why it matters
Redirects sit on critical paths: HTTP to HTTPS, old links after a redesign, login and logout, payment returns, API version changes. When one is wrong, the symptoms are confusing: form data lost because a POST became a GET, a page that redirects until the browser gives up, old links ending on a 404, or a wrong permanent redirect that browsers keep following after the fix because they cached it.
Redirects are also a security topic: an application that redirects to any URL a parameter contains lets anyone build a link on your trusted domain that lands on a phishing page. Browsers follow redirects silently, so these defects stay hidden unless a tester looks at every hop.
Five codes, two questions
In RFC 9110, permanent (301, 308) means the resource has a new permanent URL that future references ought to use: search engines and other crawlers update their links to it, and caches may store the redirect even without caching headers. Temporary (302, 307) means the original URL stays valid. 303 is neither: it points to another resource that holds the answer to this request. The temporary codes and 303 are cached only when caching headers allow it.
On the method, RFC 9110 notes that for historical reasons a client may change POST to GET when following a 301 or 302, and browsers do so under the Fetch Standard, dropping the body. 307 and 308 forbid it: the client must repeat the same method with the same body.
301 Moved Permanently: permanent; a POST may become a GET. For pages that moved for good.302 Found: temporary; a POST may become a GET. For a short detour, such as to the login page.303 See Other: the next request is a GET (a HEAD stays a HEAD). For the result page after a form POST (Post/Redirect/Get), so a refresh does not resubmit it.307 Temporary Redirect: temporary; method and body kept. For a temporary move of a POST endpoint.308 Permanent Redirect: permanent; method and body kept. For an API endpoint that moved for good.304 Not Modifiedis also a 3xx code, but it sends the client nowhere new: it has noLocationand tells the client to reuse its cached copy after a conditional request (RFC 9110 calls this redirection to a previously stored result), as in the caching lesson.
| Code | Permanent or temporary | Next request after a POST | Cacheable by defaultwithout caching headers | Typical use |
|---|---|---|---|---|
301Moved Permanently | Permanent | May become GET, body dropped | Yes | A page that moved for good |
302Found | Temporary | May become GET, body dropped | No | A short detour, such as to the login page |
303See Other | Neither: points to the answer | Always GET, body dropped | No | The result page after a form POST |
307Temporary Redirect | Temporary | POST again, same body | No | A temporary move of a POST endpoint |
308Permanent Redirect | Permanent | POST again, same body | Yes | An API endpoint that moved for good |
- Method and body kept
Worked example: an order lost in a redirect
You met this defect as row R8 of the exercise in HTTP status codes: an app followed a 301 with a GET, and the chain ended in a misleading 200. Here is how it looks in the browser, hop by hop. A web shop moves its order API from /api/v1/orders to /api/v2/orders and adds a 301 from the old path for pages still open in customers' browsers. Those customers then press "Place order" and see "Something went wrong". In DevTools, with "Preserve log" on, the Network panel shows two requests for one click (see the trace below).
The browser followed the 301 as the Fetch Standard requires for a POST: it sent a GET without the body, and the page only saw the final 405. The bug report lists both hops and names the likely cause, 301 instead of 308. After the fix, the confirmation test checks that the POST reaches v2 with its body, returns 201 Created and creates exactly one order.
Outside the browser, curl -i shows the redirect response itself, since curl follows redirects only with -L; the Redirect Checker lists every hop of a public URL, requesting each hop with GET. For every redirect, check:
- Status: the exact code the requirement needs, not just any 3xx.
Location: the right target, onhttps, keeping path and query string when it should (/old?id=7→/new?id=7).- Hops: as few as possible, ideally one.
- End: the expected page or resource, not a 404 or a login page.
- Method and body: what actually arrived after a redirected POST.
--- request 1 (the click) ---
POST /api/v1/orders HTTP/1.1
Host: shop.example.com
Content-Type: application/json
{"sku":"A-100","qty":2}
HTTP/1.1 301 Moved Permanently
Location: /api/v2/orders
--- request 2 (sent by the browser: method changed, body dropped) ---
GET /api/v2/orders HTTP/1.1
Host: shop.example.com
HTTP/1.1 405 Method Not Allowed
Allow: POSTRedirect chains and loops
A redirect chain is several redirects in a row: http://example.com/shoes → https://example.com/shoes → https://www.example.com/shoes → https://www.example.com/shoes/. Each hop costs an extra round trip and is one more rule that can break. Chains grow in layers: a load balancer upgrades to HTTPS, the web server adds www, the application adds a trailing slash. Send every old URL straight to its final address instead.
A redirect loop is a chain that never ends. Browsers give up (the Fetch Standard stops at 20 redirects) and show an error such as ERR_TOO_MANY_REDIRECTS in Chrome. Loops come from two components that disagree:
- A proxy or load balancer accepts HTTPS but forwards the request to the application over plain HTTP; the application, which also forces HTTPS, sees HTTP and redirects to the same
httpsaddress again, on every request. - The web server adds a trailing slash and the application removes it.
- The login page sends signed-in users to the dashboard, which sends users it treats as signed out back to login, for example because the session cookie was set for another domain or path.
The open redirect risk
Many applications redirect to a URL taken from the request: after login, ?next=/orders/15 returns the user to the page they wanted. If any value is accepted, https://shop.example.com/login?next=https://example.org/ sends users from your trusted domain to any site. This open redirect (or unvalidated redirect) serves phishing well, because the link starts with your real domain.
The OWASP cheat sheet on unvalidated redirects recommends avoiding user-supplied targets, mapping a short id to a URL kept on the server, or checking the target against an allow-list. Test only systems you own or have written permission to test, in a test environment, with the reserved domain example.org as the outside target. Try each value below; expect a page of your own site or a safe default every time.
- An absolute URL:
https://example.org/. - A protocol-relative URL, which browsers treat as external:
//example.org/. - A lookalike that starts with your domain:
https://shop.example.com.example.org/. - Your domain as user info before
@, which still leads toexample.org:https://shop.example.com@example.org/. - Other places: parameters such as
returnUrlorredirect, logout and payment-return pages.
Common mistakes
- Checking only the final page in the address bar, never the hops, codes and
Locationvalues in between. - Accepting any 3xx as a pass: 301 or 302 decides whether the move is permanent, 302 or 307 whether a POST keeps its body.
- Retesting a fixed permanent redirect in a browser that cached the old one. Use a clean profile or curl.
- Testing only as a new visitor, when loops often appear only with an existing session, a language cookie or a particular starting URL.
- Dismissing an open redirect as harmless, or probing it with real third-party sites instead of a reserved domain.
The QA perspective
Redirects belong in every change that moves a URL: a redesign, a new API version, a domain or HTTPS move. Ask for the redirect map (old URL, new URL, status code, query string kept or not) and turn it into a data-driven test with one row per old URL. Run it again after the deploy, because CDN and server rules often differ between environments.
In a redirect bug report, list every hop with its method, URL, status and Location, and name the rule the hops point to as a hypothesis for the team to confirm. Report an open redirect through your team's process for security defects.
Practice exercise
Review three redirect traces
Site migration requirements. R1: every old URL reaches its new page with one permanent redirect, on HTTPS, keeping the query string. R2: after sign-in, the user returns to the page in ?next=, which must belong to this site. Trace C below is a successful sign-in with a test account.
Decide whether each trace meets them; for each failing trace, write the expected trace and a one-line bug title.
Trace A GET http://example.com/sale?src=mail
301 Location: https://example.com/sale?src=mail
301 Location: https://www.example.com/sale?src=mail
302 Location: https://www.example.com/offers
200 OK
Trace B GET https://www.example.com/help
301 Location: https://www.example.com/help/
301 Location: https://www.example.com/help
301 Location: https://www.example.com/help/
... ERR_TOO_MANY_REDIRECTS
Trace C POST https://www.example.com/login?next=https://example.org/
303 Location: https://example.org/- Count the hops in each trace.
- Check every status code against R1 and R2.
- Check every
Location: scheme, host, path and query string. - Write the verdict, the expected trace and a bug title.
Show the model answer
Trace A fails R1 three ways: three hops instead of one, a temporary 302 at the end, and the query string src=mail lost on that hop. Expected: http://example.com/sale?src=mail → 301 → https://www.example.com/offers?src=mail → 200. Title: "Old /sale URL: three redirects, a final 302, query string dropped".
Trace B is a loop: one rule adds the trailing slash, another removes it, so the help page is unreachable. Expected: one canonical form, reached with at most one 301. Title: "Redirect loop on /help: trailing slash added and removed in turn".
Trace C fails R2: it is an open redirect. The 303 after the login POST is right; the target is the defect. Expected: the external value is rejected and the user lands on a default page such as /account. Report it as a security defect.
Frequently asked questions
What is the difference between 302 and 307?
Both are temporary. When following a 302, a client may turn a POST into a GET, and browsers do; with a 307 it must repeat the same method and the same body. For a GET request the two behave the same.
Should I use 301 or 308?
For pages that are only ever requested with GET, either works, and 301 is the long-established choice. For an API endpoint or a form that receives POST, use 308 so that the method and body are kept.
Why does Chrome DevTools show 307 Internal Redirect when my server sends a 301?
The browser redirected the request itself before sending it, for example because the site uses HSTS and the browser upgrades http to https on its own. That row is not a server response. To see what the server really returns, use curl or the Redirect Checker.
References
- RFC 9110: HTTP SemanticsThe meaning of HTTP methods, header fields, status codes and content negotiation, independent of the wire version.
- Fetch StandardDefines CORS: preflight requests, the Access-Control-* headers and which responses a browser may expose to a page.
- OWASP Cheat Sheet Series: Unvalidated Redirects and ForwardsOpen redirects: why a redirect target must not come from user input, and the defences — server-side mapping of identifiers to URLs, allow-lists and confirmation pages.
- Redirections in HTTP — MDNHTTP redirects: 3xx status codes with a Location header, permanent (301, 308) versus temporary (302, 303, 307), and how redirect loops arise.
Practise with Mutqan tools
Trace redirect chains (301/302/307/308) and see every hop, status and final URL.
Look up any HTTP status code with its meaning, RFC reference and when to use it.
Build cURL commands from method, URL, headers, body and auth options.
Parse raw HTTP request/response headers into a structured table with explanations.
Break a URL into protocol, host, port, path, query parameters, fragment and credentials.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.