不具合はこう現れます。ある連携があなたのエンドポイントに POST を送る。エンドポイントは動いており、ルートも存在し、手で確かめれば何も問題がない。ところがログには、POST しか受け付けないルートに対する GET が並び、404 か 405 が返っています。
原因はほぼ常に同じです。クライアントとエンドポイントの間に 301 か 302 のリダイレクトが挟まっている。そしてこれらのコードは、クライアントにメソッドの維持を義務づけません。
確認は十秒で済みます。
curl -sS -o /dev/null -L -d 'test=1' \
-w 'リダイレクト後のメソッド: %{method}\n' \
'https://httpbingo.org/redirect-to?url=/anything&status_code=301'
出力は リダイレクト後のメソッド: GET。リクエストボディも一緒に失われます。
同じコマンドの 301 を 308 に替えれば POST が返ります。違いはステータスコードだけです。
五つのコードと二つの性質
リダイレクトは独立した二つの軸で分かれます。恒久か一時か、そしてリクエストメソッドが保たれるか。
| コード | 名称 | 恒久性 | メソッド保持 |
|---|---|---|---|
| 301 | Moved Permanently | 恒久 | されない |
| 302 | Found | 一時 | されない |
| 303 | See Other | — | されない、常に GET |
| 307 | Temporary Redirect | 一時 | される |
| 308 | Permanent Redirect | 恒久 | される |
対称性に注目してください。307 はメソッド保証つきの 302 であり、308 はメソッド保証つきの 301 です。
この混乱の出どころ
仕様上、301 と 302 でメソッドを変えることは一度も要求されていません。しかし初期のブラウザやクライアントがまさにそれをやっており、食い違いが認識された頃にはウェブ全体がすでにそのように動いていました。
そこでこの挙動は「許容される」ものとして明文化されました。現行のHTTPセマンティクス仕様である RFC 9110 は、301 と 302 においてクライアントが POST を GET に置き換えることを許しています。許すが要求はしない——つまり挙動はクライアント任せであり、当てにはできません。
曖昧さのない選択肢を用意するために導入されたのが 307 と 308 です。これらではメソッドの変更が禁止されており、クライアントは同じメソッドと同じボディでリクエストを再送しなければなりません。308 は独立した RFC 7538 で定義され、MDNのドキュメントにも参照用の記述があります。
実務上の結論。301 と 302 が安全なのは、リクエストが確実に GET である場所だけです。 POST が来うる場所には 307 か 308 が必要です。
二つ目の落とし穴:恒久リダイレクトはキャッシュされる
こちらはメソッドの問題以上に忘れられがちです。
301 と 308 は移転を恒久と宣言するため、ブラウザはそれを長期間キャッシュする権利を持ちます。場合によってはユーザーがキャッシュを消すまでです。それ以降、サーバーには問い合わせすら行きません。
ここから規則が導かれます。誤って置いた 301 はほぼ取り消せません。 設定は直せますが、すでに古い応答を受け取った訪問者はリダイレクトされ続けます。302 と 307 にこの問題はありません。定義上、一時的だからです。
実務的な助言:移転の設計が固まるまでは一時コードを使ってください。あとから 302 を 301 に格上げするのは簡単ですが、その逆は簡単ではありません。
何を選ぶか
ページが恒久的に移転し、GET しか受け付けない → 301。典型例は古い記事のURL、アドレス構造の変更です。
同じだが、エンドポイントが POST を受け付ける → 308。API、Webhook、フォームがここに入ります。
遷移先が一時的に変わる、GET のみ → 302。
一時的で、メソッドが保たれなければならない → 307。
POST のあとユーザーを結果ページへ送る → 303。これは POST/Redirect/GET パターンです。ページの再読み込みでフォームが再送信されないよう、意図的にリクエストを GET に変えます。ここではメソッドの変更が目的であり、欠陥ではありません。
実際にどこで壊れるか
APIの手前にある http→https リダイレクト。 最も多く、最も厄介な場面です。クライアントは http:// で設定されており、サーバーは https:// を指す 301 を返し、メソッドが失われます。症状は、POST ルートに対する GET がログに並ぶこと。301 を 308 に替えれば解決します。
ホスト名の正規化。 www からなしへ(またはその逆)のリダイレクトは通常 301 で組まれます。ページには問題ありませんが、同じホスト上にAPIが同居していれば、まったく同じ問題が当てはまります。
フォーム送信。 フォームがリダイレクトするアドレスに送信される。データがハンドラーに届いていないため、ユーザーには「何も起きなかった」ように見えます。
Webhook。 外部サービスが、かつてあなたが伝えたアドレスへ POST を送る。そのアドレスが 301 の裏に移動していれば、Webhookは静かに動かなくなり、送信側のログにはリダイレクトへの成功応答だけが残ります。
四つに共通する特徴があります。エンドポイントは健全なのに、リクエストが元の形で届いていない。だから調査が的外れな方向へ進みます。ハンドラーのバグを探しますが、原因はウェブサーバーの設定にあるのです。
短縮サービスが302を使う理由
短縮リンクはほぼ常に 302 を返します。これは怠慢ではなく意図的な選択です。
理由は恒久性であって、メソッドではありません。短縮リンクの遷移先は変わりうるからです。301 を使えばブラウザにその遷移を恒久的にキャッシュさせることになり、遷移先を変えたあとも一部の訪問者は古いアドレスに着き続けます。ここでメソッドは無関係です——短縮リンクは GET リクエストでたどられます。
仕組み自体は短縮URLの仕組みで別途扱っています。
コードの選択で変わらないこと
広く見られる二つの誤解。
リンク評価。 301 と 302 のどちらを選ぶかは評価の受け渡しに影響しません——Googleは2016年に確認済みです。詳しくは短縮リンクはリンク評価を渡すのかにあります。
正規URLの選定。 リダイレクトと rel="canonical" は別の問題を解きます。リダイレクトはアドレスを流通から外し、canonical は両方を到達可能なまま主たるものを指し示します。使い分けはcanonical完全ガイドに書いてあります。
短いチェックリスト
POSTを受け付けるルートでは、302 や 301 ではなく 307 か 308 を使う。- http→https リダイレクトは個別に確認する。一括ルールで設定され、APIが見落とされがち。
- 設計が固まるまでは一時コードを。301 と 308 はキャッシュされ、ほぼ元に戻せない。
- フォーム処理のあとは 303 を。これは正しい挙動であって誤りではない。
- 連鎖は1ホップに抑える。余分なホップは遅延とリスクの両方を増やす。