307 と POST リクエスト|内容が届かない・二重登録されるときの確認ポイント
フォームから送信した内容がサーバーに届いていない。API に POST したのに、更新が反映されない。逆に、注文が二重に登録されてしまった。こうした不具合の裏に、リダイレクトが潜んでいることがあります。
POST は「サーバーの状態を変える」ための通信です。GET のように、何度繰り返しても結果が変わらない通信ではありません。そのため、転送の途中でメソッドや本文が変わると、結果が大きく変わります。そして、多くの場合、エラーとして表示されないため、気づくのが遅れます。
307 の約束
POST のまま、本文も変えずに、移動先へ送り直します。
302 の揺れ
POST が GET に変わり、本文が捨てられることがあります。
303 の役割
POST の後に、結果ページを GET で取得させます。
確認方法
curl -v で、転送の前後のメソッドと本文を見比べます。
内容が届かない・二重に処理される、本当の怖さ
POST に絡む転送の不具合は、一般のページの転送とは比べものにならないほど厄介です。
エラーにならずに失敗する
POST が GET に変わって転送先へ届くと、転送先のサーバーは「普通の GET 要求」として応答します。多くの場合、一覧画面やトップページが 200 で返ってきます。画面上は正常に見えますが、登録も更新も実行されていません。利用者は「送信できた」と思い込み、運営者も、エラーのログが残らないため、気づけません。
二重に処理される
逆に、転送のたびに処理が実行されると、二重に登録されます。たとえば、転送元の URL でも処理を実行してから 307 を返す実装だと、転送先でも同じ内容が処理されます。決済や注文のような処理では、重大な事故につながります。
大きなデータで負荷が倍になる
307 や 308 は本文を再送します。ファイルのアップロードのように本文が大きい通信では、転送のたびに同じデータを送り直すため、通信量と時間が増えます。
本文を再送できずに失敗する
WHATWG の Fetch Standard には、本文の再送について重要な規定があります。ブラウザーの fetch で、ストリームのような一度しか読めない本文を送っている場合、307 や 308 を受け取っても再送できず、ネットワークエラーとして扱われることがあります。「転送されるはずが、通信自体が失敗する」という、分かりにくい症状になります。
番号による動きの違いを整理する
こうした不具合を防ぐには、まず番号ごとの動きを正確に理解しておく必要があります。
| 返した番号 | POST の扱い(Fetch Standard の手順) | 本文 |
|---|---|---|
| 301 | GET に変わる | 捨てられる |
| 302 | GET に変わる | 捨てられる |
| 303 | GET に変わる(GET・HEAD 以外の場合) | 捨てられる |
| 307 | POST のまま | 保たれる |
| 308 | POST のまま | 保たれる |
RFC 9110 の規定では、301 と 302 は POST を GET に変えてもよい(MAY)ですが、変えるかどうかは実装に任されています。307 は、自動で転送する場合にメソッドを変えてはならない(MUST NOT)と定められています。
つまり、POST を確実に届けたいなら 307(一時的)か 308(恒久的)、POST の後に結果画面を表示したいなら 303、という選び方になります。
目的別の設計パターン
パターン 1: POST の後に結果ページを見せる(303)
もっとも一般的な場面です。注文確定や問い合わせ送信の処理を終えたら、「完了しました」の画面を表示します。ここで、処理を行った URL に直接結果を返すと、利用者が画面を更新するたびに、同じ POST が再送されます。
そこで、処理を終えたら 303 で結果ページへ転送し、結果ページは GET で表示します。更新しても、結果ページを GET で取り直すだけなので、二重に処理されません。「POST の後に転送して GET で表示する」この方式は、Post/Redirect/Get と呼ばれることがあります。
clientserver
パターン 2: POST の処理を、別の URL へ引き継ぐ(307 または 308)
API の移行や、処理を担当するサーバーの切り替えのように、同じ POST を別の場所で処理してもらう場面です。メソッドと本文が保たれるので、移動先は元の要求とまったく同じ内容を受け取れます。一時的な切り替えなら 307、恒久的な移転なら 308 を使います。
clientserver
パターン 3: 転送を使わず、最初から正しい宛先に送らせる
もっとも確実なのは、転送そのものを避けることです。API の仕様書やクライアントの設定で、正しい URL を案内しておけば、本文の再送も、環境による動作のばらつきも発生しません。転送は、移行期間の保険として置いておくくらいの位置づけが適切です。
curl で動きを再現して確かめる
実際に何が起きているのかは、curl で確かめられます。-v を付けると、送信と受信の詳細が表示されます。
curl -v -L -X POST -d 'name=pen' https://example.com/old-endpoint
出力では、> で始まる行が送信、< で始まる行が受信です。次の点を見比べます。
- 最初の要求のメソッド(
POST)と、応答の番号(301・302・303・307・308 のどれか)。 - 転送後の要求のメソッド(
POSTのままか、GETに変わったか)。 - 転送後の要求に、本文(
Content-Lengthなど)が付いているか。
curl のマニュアルによれば、-L を付けて POST を送ると、301・302・303 の応答では次の要求が GET になり、それ以外の 3xx(307・308 を含む)では元のメソッドが保たれます。--post301、--post302、--post303 を付けると、対応する番号でも POST のままで送り続けます。
ブラウザーの fetch でも動作を制御できます。redirect オプションに follow(転送を追う。既定)、error(転送されたらエラーにする)、manual(転送を追わず、自分で処理する)を指定できます。転送が起きること自体を検出したい場合に便利です。
原因別の対処
| 症状 | 考えられる原因 | 対処 |
|---|---|---|
| 送信後に一覧が出るだけで、登録されていない | 301・302 で POST が GET に変わった | 転送を 307 / 308 に変える。または転送を使わず正しい URL に送る |
| 画面の更新で二重登録される | POST の結果をそのまま表示している | 処理後に 303 で結果ページへ転送する |
| 転送先で 405 Method Not Allowed になる | 転送先が POST を受け付けない | 転送先の設定を確認する。307 でメソッドが保たれた結果として表面化している |
| 転送が起きた途端に通信が失敗する | ストリーム本文を再送できない | 本文の形式を見直す。転送を使わず直接送る |
| 転送の前後で二重に処理される | 転送元でも処理を実行している | 転送元では処理せず、案内だけを返す |
転送を返す側が守りたいこと
最後に、転送を返す側の鉄則を挙げます。
- 転送を返す URL では、処理をしない。 案内だけを返し、処理は移動先に任せます。
- メソッドを保ちたいなら、301・302 ではなく 307・308 を返す。 実装に任せず、仕様で動作を固定します。
- 結果の表示には 303 を使う。 二重送信を防げます。
- 移動先がメソッドを受け付けるか、事前に確認する。 307 で POST が届いた結果、移動先で拒否されることがあります。
303 で結果ページへ転送する実装例(PHP)
Post/Redirect/Get は、多くの Web アプリケーションで定番の実装です。PHP では、処理が終わったあとに header() で 303 を返します。
<?php
// 注文を登録する処理(省略)
save_order($_POST);
// 完了ページへ GET で転送する。後続の処理が動かないよう exit する。
header('Location: /order/complete', true, 303);
exit;
PHP のマニュアルによれば、Location ヘッダーを送ると、すでに 201 か 3xx が設定されていない限り 302 が返されます。303 を返したいときは、第 3 引数で応答コードを明示します。この指定を忘れて既定の 302 のまま運用していても、現在の主要なブラウザーでは POST が GET に変わるため、多くの場合は動作します。ただし、意図を仕様で固定する意味でも、303 を明示しておくのが望ましい書き方です。
サーバーのログで見分ける
転送で問題が起きているかどうかは、アクセスログからも推測できます。次の観点で、該当の時刻のログを見比べてください。
- 転送元の URL に、POST の記録があるか。 あれば、要求は届いています。
- 転送先の URL に、POST の記録があるか、GET の記録だけか。 GET だけなら、メソッドが変わっています。
- 同じ内容の POST が複数回記録されていないか。 あれば、二重処理が起きています。
- 応答の番号が、設計どおりか。 転送元が 307 や 303 を返しているか、意図しない 302 になっていないかを確認します。
ログの形式によっては、メソッドと番号が同じ行に出ます。転送の前後で行が分かれる点に注意して、時系列に並べて読むと見通しがよくなります。
転送を介する前に、通信の目的を問い直す
POST の転送で起きる不具合の多くは、「この通信は何のために転送されるのか」が曖昧なまま設定されたことが原因です。
目的が「利用者に結果を見せること」なら 303、「別の場所に処理を引き継ぐこと」なら 307 か 308、「そもそも転送が不要」なら、クライアント側の宛先を直す。このように、目的を言葉にすると、番号は自然に決まります。逆に、目的が説明できない転送は、見直しの候補です。設定の履歴を整理し、不要な転送を減らすことも、POST の不具合を防ぐ有効な手段です。新しく転送を追加するときは、「どの URL に、どのメソッドが届き、移動先で何が起きるか」を一行で書き出してから設定する習慣をつけると、設計の抜けに気づきやすくなります。
POST の転送は「番号の選び方」で決まる
要点を整理します。
- POST が絡む転送の不具合は、エラーにならずに結果だけが変わるため、見つけにくいものです。
- 301・302 は POST を GET に変えうるのに対し、307・308 は変えません。結果ページの表示には 303 を使います。
- 本文の大きな通信や、ストリーム本文では、転送のたびに再送の負担や失敗のリスクがあります。
- 確認は、
curl -vで転送前後のメソッドと本文を見比べるのが確実です。
転送の番号の全体像は301・302・303・307・308 の違いに、302 と 307 の比較は307 と 302 の違いにまとめています。