Skip to content
Control Plane Labs

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.

Other 3xx codes