When Cloudflare redirects your POST request with a 301, the browser silently turns it into a GET — dropping the request body, stripping your Content-Type, and leaving your backend wondering why it received an empty GET to a POST-only endpoint. The fix is a one-field change in your Cloudflare dashboard: switch that redirect from 301 to 308. It takes about thirty seconds to deploy and requires no code changes on your backend.
The symptom
Your server logs show two requests where you only sent one — and the second one has the wrong HTTP method:
POST /api/submit HTTP/1.1 → 301 Moved Permanently
GET /api/submit HTTP/1.1 → 200 OKThe browser sent a GET. You sent a POST. The redirect ate the method, and nobody told you.
The most common places this bites you: Cloudflare HTTP-to-HTTPS redirects, non-www-to-www (or vice versa) permanent redirects, and domain migration redirect rules. All of these are typically set up as 301s. All of them silently break POST requests from browser clients.
What I was trying to do
I was deploying a Next.js app with a form submission endpoint behind Cloudflare. The setup was simple: a Cloudflare Redirect Rule to push all http:// traffic to https://, plus a non-www to www redirect. Both rules set to 301 Permanent Redirect — the default Cloudflare behavior, standard infra practice, the thing every infrastructure tutorial tells you to do.
GET requests worked fine. Pages loaded, API fetches succeeded, health checks returned green. Then a user reported that the signup form was silently doing nothing. No error state. No network error in the console. Just a completed request that had no effect on the backend.
Server logs showed the endpoint was receiving a GET with an empty body. The form route was POST-only. The backend was logging 405 Method Not Allowed and not surfacing it to the client. The user saw a spinner that completed and nothing else happened.
The wrong path I went down first
My first instinct was CORS. Cross-origin form submissions plus a redirect — that’s exactly where CORS breaks in practice. I spent close to an hour adding OPTIONS handling to the route, relaxing Access-Control-Allow-Methods, double-checking that POST was explicitly in the allowed methods header, and running curl -X OPTIONS tests against the endpoint.
None of it helped. CORS was clean.
Then I suspected Content-Type stripping. Maybe Cloudflare was dropping headers during the redirect. I tried hardcoding Content-Type: application/json on every request, added it to the CORS allowed headers list on the server, verified the preflight response included it. Same result every time.
The misleading part: when I tested directly against the https://www. URL — bypassing the redirect entirely — the POST worked perfectly. So I kept drilling into server configuration and client-side fetch logic, completely ignoring the redirect sitting in between them.
What finally cracked it was slowing down and reading the server log output in full instead of scanning for the last line. One form submission, two log entries. POST /api/submit → 301, then GET /api/submit → 200. Once I saw that sequence, the redirect was obviously the culprit. I’d been looking at the wrong layer the entire time. This took me about four hours and one unnecessarily long debugging session to figure out.
What’s actually happening
This is not a Cloudflare bug, and it is not a browser bug either. It is documented HTTP behavior that has existed since the 1990s, and it still catches developers off guard on a regular basis because the default redirect status code in almost every tool — Cloudflare, nginx, Apache, load balancers — is 301.
RFC 7231 describes 301 Moved Permanently like this:
> “Note: For historical reasons, a user agent MAY change the request method from POST to GET for the subsequent request.”
“MAY” in RFC language means every major browser in the wild does it. Chrome, Firefox, Safari, Edge — they all convert POST to GET on 301 and 302 redirects. They have done this since the early web, the spec was written to formalize existing browser behavior, and they will keep doing it indefinitely.
This is precisely why HTTP 307 and 308 were added to the specification:
| Status Code | Name | Preserves Method? | Permanent? |
|————|——|——————-|————|
| 301 | Moved Permanently | No (POST → GET) | Yes |
| 302 | Found | No (POST → GET) | No |
| 307 | Temporary Redirect | Yes | No |
| 308 | Permanent Redirect | Yes | Yes |
307 and 308 explicitly require the client to repeat the request with the same method and body to the new location. No conversion. No dropped payload. If you sent a POST, the redirect destination receives a POST.
Cloudflare Redirect Rules default to 301. That is what the dashboard pre-selects, what most infrastructure guides recommend for permanent redirects, and what is quietly breaking POST-heavy APIs across the internet at this exact moment.
The fix
Via Redirect Rules (Cloudflare dashboard):
- Go to your zone dashboard → Rules → Redirect Rules
- Edit the rule handling your HTTP-to-HTTPS or domain redirect
- In the “Then the URL redirects to…” section, change the Status code dropdown from
301to308 - Save and deploy
Via Bulk Redirects:
- Account Home → Bulk Redirects → edit your redirect list
- Click the three-dot menu on the affected rule → Edit
- Expand Advanced Options and change the status code to
308
Via Page Rules (legacy):
- Rules → Page Rules → edit the forwarding rule
- Change
301 - Permanent Redirectto308 - Permanent Redirectin the dropdown
Why this works: 308 tells the browser: “this location has permanently moved — resend the request with the exact same method and body to the new URL.” Browsers are required by spec to comply. Your POST stays a POST, your request body arrives intact.
No cache purge is needed after this change. 308 and 301 are distinct response codes with separate cache entries. The change takes effect immediately for new requests.
Verification
Test with curl before and after to confirm the method is preserved:
“`bash
Before the fix — watch the method change
curl -v -X POST \
-H “Content-Type: application/json” \
-d ‘{“test”: true}’ \
http://yourdomain.com/api/endpoint 2>&1 | grep -E “< HTTP|> (POST|GET)”
Output before fix:
> POST /api/endpoint HTTP/1.1
< HTTP/1.1 301 Moved Permanently
> GET /api/endpoint HTTP/1.1 ← method changed, body dropped
Output after switching to 308:
> POST /api/endpoint HTTP/1.1
< HTTP/1.1 308 Permanent Redirect
> POST /api/endpoint HTTP/1.1 ← method preserved, body intact
In Chrome DevTools → Network tab, the difference shows as two requests: before the fix you see 301 followed by a GET with no payload; after the fix, 308 followed by a POST with your original request body visible in the payload preview.
Quick smoke test to verify end-to-end:
```bash
curl -s -o /dev/null -w "redirect: %{redirect_url}\nfinal_method_hint: check_server_logs\nhttp_code: %{http_code}\n" \
-X POST http://yourdomain.com/api/endpointIf the final response from your server comes back with the expected POST handler output (not a 405), the fix held.
Edge cases and gotchas
nginx ingress also defaults to 301. In a Kubernetes setup running both Cloudflare and an nginx ingress controller, you can have this problem at both layers independently. The nginx fix is identical: change return 301 to return 308 in your server block or ingress annotation. A common pattern in Cloudflare community threads is teams migrating from nginx ingress (which was already using 308) to Cloudflare redirect rules (which defaulted back to 301), and discovering their POST endpoints broke in production the same day.
Mobile HTTP clients often do NOT convert POST to GET on 301. Libraries like requests (Python), axios, URLSession (iOS), and Android’s HttpURLConnection commonly preserve POST through 301 redirects — unlike browsers. Your backend integration tests and Postman collections will pass cleanly while browser-based form submissions silently fail. Do not let clean non-browser test results convince you there is no problem.
307 vs 308 — use the right one. If your redirect is temporary — maintenance window, feature-flag routing, A/B traffic split — use 307, not 308. Returning 308 on a temporary redirect signals to search engine crawlers that the original URL is permanently dead. They will update their index and pass link equity to the new URL. Use 307 to preserve the SEO status of the original URL while still protecting POST method behavior.
Cloudflare’s “Always Use HTTPS” toggle issues 301. If you are using the built-in Always Use HTTPS toggle in SSL/TLS settings instead of a manual redirect rule, it also sends 301 redirects and the same problem applies. The only way to override this behavior is to disable that toggle and replace it with a manual Redirect Rule configured to status code 308.
TL;DR fix
In Cloudflare Redirect Rules, change the status code from 301 to 308 — permanent redirect that preserves the HTTP method and request body.
