フォームから送信した内容がサーバーに届いていない。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

POST /order (注文内容)
サーバーは、注文を登録してから 303 を返します
303 See Other / Location: /order/complete
GET /order/complete
200 OK (完了画面)
画面を更新しても、GET が繰り返されるだけです

パターン 2: POST の処理を、別の URL へ引き継ぐ(307 または 308)

API の移行や、処理を担当するサーバーの切り替えのように、同じ POST を別の場所で処理してもらう場面です。メソッドと本文が保たれるので、移動先は元の要求とまったく同じ内容を受け取れます。一時的な切り替えなら 307、恒久的な移転なら 308 を使います。

clientserver

POST /api/orders (JSON 本文)
307 Temporary Redirect / Location: https://api2.example.com/orders
メソッドも本文も保たれます
POST https://api2.example.com/orders (同じ JSON 本文)
201 Created

パターン 3: 転送を使わず、最初から正しい宛先に送らせる

もっとも確実なのは、転送そのものを避けることです。API の仕様書やクライアントの設定で、正しい URL を案内しておけば、本文の再送も、環境による動作のばらつきも発生しません。転送は、移行期間の保険として置いておくくらいの位置づけが適切です。

curl で動きを再現して確かめる

実際に何が起きているのかは、curl で確かめられます。-v を付けると、送信と受信の詳細が表示されます。

curl -v -L -X POST -d 'name=pen' https://example.com/old-endpoint

出力では、> で始まる行が送信、< で始まる行が受信です。次の点を見比べます。

  1. 最初の要求のメソッド(POST)と、応答の番号(301・302・303・307・308 のどれか)。
  2. 転送後の要求のメソッド(POST のままか、GET に変わったか)。
  3. 転送後の要求に、本文(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 の違いにまとめています。