The 302 Redirect Turned Your POST into a GET and Dropped the Body
Quick answer
If your client sends a POST with a JSON body and the server responds with a 302, most HTTP clients follow the redirect automatically and reissue the request as a GET, dropping the body entirely. The handler at the final URL then receives an empty GET and you see a confusing 405, 400, or a record that never got created. The bug is rarely in the final endpoint; it is in the redirect that changed the method. Stop the client from auto-following, inspect each hop, and decide whether the server should return a 307/308 instead of a 302.
What the client actually does
Here is a normal create call. The expectation is a 201 with the new resource.
const res = await fetch("https://api.example.com/v1/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sku: "WIDGET-9", qty: 2 }),
});
console.log(res.status, res.url);
What happens on the wire is a redirect first, because the old path moved to a versioned route.
POST /v1/orders HTTP/1.1
Host: api.example.com
HTTP/1.1 302 Found
Location: https://api.example.com/v2/orders
Per the historical rules for 301 and 302, a user agent may rewrite a POST to GET on redirect, and the major client libraries do exactly that. The second request is therefore GET /v2/orders with no body. The v2 create route either rejects the method or, worse, responds with the list endpoint and your code treats it as success.
Why the error message misleads you
Because the redirect is followed transparently, the only response your code sees is the final one. You never observe the 302 in application logs unless you log the redirect chain. The symptoms point everywhere except the real cause:
| Symptom at the final URL | What it looks like | Actual cause |
|---|---|---|
| 405 Method Not Allowed | The route does not accept your verb | POST was rewritten to GET |
| 400 validation error | Your payload is malformed | The body was dropped, fields are missing |
| 200 with a list | The create succeeded somehow | You hit the GET collection endpoint |
| Empty record in the database | A serialization bug | A GET never created anything |
Inspect the hops instead of guessing
Turn off automatic redirects and walk the chain manually. With curl the -I flag shows the first response, and --trace reveals every hop.
curl -sS -X POST "https://api.example.com/v1/orders" \
-H "Content-Type: application/json" \
-d '{"sku":"WIDGET-9","qty":2}' \
-o /dev/null -w "status=%{http_code} redirect=%{redirect_url}\n"
In a program, disable following and handle the Location yourself so the method stays explicit.
const direct = await fetch(url, { ...options, redirect: "manual" });
if (direct.status === 302) {
const next = direct.headers.get("location");
// Re-POST to the new URL yourself; do not let the client downgrade it.
return fetch(next, options);
}
Fix the server, not just the client
The robust fix is to return a status code that preserves the method. 307 Temporary Redirect and 308 Permanent Redirect require the client to keep both the HTTP method and the request body. Use these for API routes; reserve 301/302 for navigation where a GET is genuinely intended.
| Status | Method on follow | Body kept | Typical use |
|---|---|---|---|
| 301 / 302 | POST often becomes GET | No | Browser navigation, legacy |
| 307 | Same method | Yes | Temporary API move |
| 308 | Same method | Yes | Permanent API move |
When you change the server code, confirm the new response in a tunnel or staging before deploying, and watch for a double hop: a 302 that lands on another 302 compounds the problem and makes the chain harder to reason about.
The authentication wrinkle
Redirects also interact with credentials. A client may strip an Authorization header when the Location crosses a host or subdomain, so even a correct 307 can arrive unauthenticated. If the redirect target is on a different origin, treat it as a new request and attach the appropriate token explicitly rather than assuming the header traveled with it.
FAQ
Why does this work in the browser but fail in my server code?
Different clients apply the redirect rules differently. A browser form and a backend HTTP library have separate default behaviors around method rewriting and header forwarding. Never assume two clients follow the same 302 the same way; verify the actual chain for the client you use.
Can I just change the client to always re-POST?
That handles the immediate case but leaves every other consumer hitting the same bad 302. Fixing the server to return 307/308 solves it for all clients. Use a manual client redirect only as a stopgap while the server change is rolled out.
How do I see redirects that happen inside an SDK?
Most SDKs accept a custom HTTP transport or an option to disable redirects. Inject a logging transport that records status and Location before the request is followed, or point the SDK at a local tunnel and capture the raw traffic.
Is a 302 to a GET ever correct for an API?
Yes, for operations that genuinely end in navigation or a resource you retrieve, such as after an HTML form submit. It is wrong when the redirected request is still a write that needs the original body and method.
This is part of the The API Debugging Guide — start there for a systematic way to trace failures that the client hides from you.