The bug looks like this. An integration sends a POST to your endpoint. The endpoint works, the route exists, everything checks out by hand. But the logs show a GET against a POST-only route, answered with 404 or 405.

The cause is almost always the same: a 301 or 302 redirect sits between the client and the endpoint. And those codes do not oblige the client to keep the method.

You can confirm it in ten seconds:

curl -sS -o /dev/null -L -d 'test=1' \
  -w 'method after redirect: %{method}\n' \
  'https://httpbingo.org/redirect-to?url=/anything&status_code=301'

Output: method after redirect: GET. The request body is lost along with it.

Change 301 to 308 in the same command and you get POST. The only difference is the status code.

Five codes, two properties

Redirects differ along two independent axes: permanent or temporary, and whether the request method survives.

Code Name Permanence Method preserved
301 Moved Permanently permanent no
302 Found temporary no
303 See Other no, always GET
307 Temporary Redirect temporary yes
308 Permanent Redirect permanent yes

Note the symmetry: 307 is 302 with a method guarantee, and 308 is 301 with a method guarantee.

Where the confusion came from

Formally, the specification never required changing the method on 301 and 302. But early browsers and clients did exactly that, and by the time the mismatch was noticed, the entire web already worked that way.

So the behaviour was codified as permitted: RFC 9110, the current HTTP semantics specification, allows a client to change POST to GET on 301 and 302. Allows, but does not require — which means the behaviour is left to the client and cannot be relied upon.

To provide an unambiguous option, 307 and 308 were introduced. For those, changing the method is forbidden: the client must repeat the request with the same method and the same body. Code 308 is defined in its own RFC 7538, with a reference description in the MDN documentation.

The practical conclusion: 301 and 302 are only safe where the request is certainly a GET. Anywhere a POST might arrive, you need 307 or 308.

The second trap: permanent redirects are cached

This one gets forgotten more often than the method problem.

301 and 308 declare the move permanent, and a browser is entitled to cache them for a long time — sometimes until the user clears their cache. After that the server is simply not asked.

Hence the rule: a mistaken 301 is close to impossible to roll back. You will fix the configuration, but visitors who already received the old response will keep being redirected. 302 and 307 do not have this problem — they are temporary by definition.

Practical advice: while a migration scheme is still settling, use the temporary codes. Promoting a 302 to a 301 later is easy; going the other way is not.

What to choose

A page has moved for good and only accepts GET → 301. The classic case: an old article URL, a change of URL structure.

The same, but the endpoint accepts POST → 308. This covers APIs, webhooks and forms.

The destination changes temporarily, GET only → 302.

Temporary, and the method must survive → 307.

After a POST, send the user to a result page → 303. This is the POST/Redirect/GET pattern: it deliberately turns the request into a GET so that refreshing the page does not resubmit the form. Here the method change is the point, not a bug.

Where this breaks in practice

An http→https redirect in front of an API. The most common and most painful case. The client is configured with http://, the server answers 301 pointing at https://, the method is lost. Symptom: GET in the logs on a POST route. Fixed by replacing 301 with 308.

Host canonicalisation. The www to non-www redirect (or the reverse) is usually set up as a 301. Fine for pages, but if an API lives on the same host, exactly the same problem applies.

Form submissions. A form posts to an address that redirects. The user sees "nothing happened" because the data never reached the handler.

Webhooks. An external service sends a POST to an address you gave it once. If that address moved behind a 301, webhooks quietly stop working, while the sender's logs show a successful response to the redirect.

All four share a signature: the endpoint is healthy but the request does not reach it in its original form. Diagnosis therefore goes the wrong way — people hunt for a bug in the handler, and it is in the web server configuration.

Why URL shorteners use 302

A short link almost always returns 302, and that is a deliberate choice rather than carelessness.

The reason is permanence, not the method: a short link's destination may change. Using 301 would let browsers cache the hop forever, and after the destination changed some visitors would keep landing on the old address. The method is irrelevant here — short links are followed with GET requests.

The mechanism itself is covered separately in how URL shorteners work.

What the choice of code does not change

Two widespread misconceptions.

Link equity. Choosing between 301 and 302 does not affect how equity passes — Google confirmed that back in 2016. The full treatment is in do short links pass link equity.

Canonical URL selection. A redirect and rel="canonical" solve different problems: a redirect removes an address from circulation, canonical leaves both reachable and names the primary one. When to use which is described in the guide to canonical.

Short checklist

  • On routes that accept POST, use 307 or 308, not 302 and 301.
  • Check the http→https redirect separately: it is often set by a blanket rule and the API gets forgotten.
  • While the scheme is still settling, use temporary codes. 301 and 308 are cached and barely reversible.
  • After processing a form, use 303 — that is correct behaviour, not a mistake.
  • Keep chains down to a single hop: every extra jump adds both latency and risk.