307 Temporary Redirect
Redirection response defined by RFC 9110 §15.4.8.
Last updated July 27, 2026
Common causes at a glance
- An API deliberately routing a write to another host without losing the body
- A regional load balancer sending mutations to the primary write region
- HSTS upgrading http to https, shown by browsers as an internal 307
- A framework redirect helper switched from 302 to preserve POST semantics
Reproduce it
curl -sSI -X POST -d 'id=1' https://example.com/api/orders | grep -iE '^(HTTP|location)'What 307 Temporary Redirect means
307 exists because 302’s behaviour and 302’s specification disagreed.
Clients rewrote POST to GET; the text said they must not. The
resolution was to leave 302 describing what browsers do and add two unambiguous codes:
307 preserves the method, 303 requires the switch to
GET.
Use 307 whenever a non-idempotent request must land elsewhere with its payload intact: an API version shim, a write routed to a primary region, a temporary maintenance target. The body is re-sent, so the client has to still have it — a streamed upload cannot always be replayed, and a large one is transferred twice.
What the spec says
RFC 9110 §15.4.8 states the user agent MUST NOT
change the request method when following the redirect automatically, and that the
client should keep using the original URI because the redirection can change over time.
Unlike 308, a 307 is not heuristically cacheable under
RFC 9111 §4.2.2, so it applies to this request only. Browsers
also generate a synthetic 307 internally when HSTS upgrades an
http:// URL, which is why one can appear in DevTools without any server
having sent it.
What actually causes it
The commonest 307 most engineers meet is not a server response at
all. When a host is preloaded or has previously sent
Strict-Transport-Security, the browser rewrites the scheme before the
request leaves and DevTools labels the entry 307 Internal Redirect. It
never touches the network, so curl will not reproduce it and your access logs will not
show it. Real 307s fail in one of two ways. Cross-origin targets strip credentials and
can trip CORS, since the preflight applies to the redirect target too. And replaying
the body is not free: a client streaming a multi-gigabyte upload may be unable to
rewind it, so the retry fails outright rather than transferring twice.
How to debug it
Verify the method actually survived rather than trusting the code:
curl -sSv -X POST -d 'id=1' -L https://example.com/api/orders 2>&1 \
| grep -E '^[<>] (POST|GET|HTTP|location)'
Both request lines should say POST. If the second is a GET,
the client library is not honouring the code; fix the client, not the server.
For the browser-only case, check whether the 307 is internal: if
curl -I http://example.com/ returns 301 while the
browser shows 307, HSTS is doing it, and your server-side redirect is never exercised
by browsers that have seen the header. Confirm by looking for
Strict-Transport-Security on the HTTPS response with the
header inspector.
Working examples
curl
# The method is preserved automatically; both hops should be POST.
curl -sSv -X POST -d 'id=1' -L https://example.com/api/orders 2>&1 \
| grep -E '^[<>] (POST|GET|HTTP|location)'
# Raw first hop only:
curl -sSI -X POST -d 'id=1' https://example.com/api/orders \
| grep -iE '^(HTTP|location)'
# Is a browser 307 actually HSTS? Compare the cleartext hop:
curl -sSI http://example.com/ | head -1
Python (requests)
import requests
# requests preserves the method across 307 and re-sends the body.
resp = requests.post(
"https://example.com/api/orders",
json={"id": 1},
timeout=10,
)
for hop in resp.history:
print(hop.status_code, hop.request.method, "->", hop.headers.get("Location"))
print("final:", resp.status_code, resp.request.method) # POST
# A file object cannot be rewound automatically; read it into memory first
# if the endpoint might answer 307.
body = open("payload.json", "rb").read()
requests.post("https://example.com/api/orders", data=body, timeout=30)
Node (fetch)
// fetch() re-sends the method and body on 307.
const resp = await fetch("https://example.com/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: 1 }),
});
console.log(resp.status, resp.redirected, resp.url);
// A ReadableStream body cannot be replayed across a redirect; buffer it
// yourself, or use redirect: "manual" and re-issue the request.
const raw = await fetch("https://example.com/api/orders", {
method: "POST",
body: JSON.stringify({ id: 1 }),
redirect: "manual",
});
console.log(raw.status, raw.headers.get("location"));Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.