😠

なんでPOSTの値がとれないんだよぉぉぉ!!

2023/01/21に公開約4,900字

普通にJavaScriptからPHPにデータ投げて、PHPで処理したかっただけなのに。。
「どうして値がとれないんだよぉぉぁぁぁぁぁ!」

ということで解決までの道のりを記していきます。
今後、私と同じ道を辿る方が出ないことを祈って。

実際のコード

早速ですが実装したコードをご覧いただきましょう。

フロントエンド

フロント側、JavaScriptのコードです。
fetchメソッドを使用し、PHPファイルにデータを投げているだけです。

main.js
const data = {
  name: 'john'
}

fetch('http://localhost/request.php', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data)
})
  .then(response => response.json())
  .then(res => {
    console.log('🍔', res)
  })
  .catch(error => {
    console.error('🍤', error)
  })

バックエンド

フロントエンドからデータを受け取って、そのまま返すようにしました。
リクエストはPOSTのみ受け付けるようにします。CORS対策のため、一旦全てのオリジンを許可するようにしています。

request.php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST");

$name = $_POST["name"];
echo json_encode($name);

初劇「なんでCORSエラーでてんだよぉ!」

早速エラー発生しました。CORS違反です。

Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.

どうやらPreflight requestが関係しているようです。
preflight requestについて調べてみると以下のように書かれていました。

CORS のプリフライトリクエストは CORS のリクエストの一つであり、サーバーが CORS プロトコルを理解していて準備がされていることを、特定のメソッドとヘッダーを使用してチェックします。
これは OPTIONS リクエストであり、 Access-Control-Request-Method,Access-Control-Request-Headers, Origin の 3つのHTTPリクエストヘッダー使用します。
プリフライトリクエストはブラウザーが自動的に発行するものであり、通常は、フロントエンドの開発者が自分でそのようなリクエストを作成する必要はありません。これはリクエストが"to be preflighted"と修飾されている場合に現れ、単純リクエストの場合は省略されます。
https://developer.mozilla.org/ja/docs/Glossary/Preflight_request

ということはこのリクエストは単純リクエストではない、ということでしょうか。
単純リクエストについては以下のように書かれています。

単純リクエストは、以下のすべての条件を満たすものです。
許可されているメソッドのうちのいずれかであること。
GET
HEAD
POST
ユーザーエージェントによって自動的に設定されたヘッダー (たとえば Connection、 User-Agent、 または Fetch 仕様書で禁止ヘッダー名として定義されているヘッダー)を除いて、手動で設定できるヘッダーは、 Fetch 仕様書で CORS セーフリストリクエストヘッダーとして定義されている以下のヘッダーだけです。
Accept
Accept-Language
Content-Language
Content-Type (但し、下記の追加の要件に注意してください)
Range (単純範囲ヘッダー値、例えば bytes=256- や bytes=127-255 の場合)


Content-Type ヘッダーで指定できるメディア種別に許されるタイプ/サブタイプの組み合わせは、以下のもののみです。
application/x-www-form-urlencoded
multipart/form-data
text/plain
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#単純リクエスト

つまり、フロント側でapplication/jsonに設定したのでPreflight requestが自動発行されたということになり、単純リクエストではなかった、といえます。

Preflight requestが必要とするのは

  • Access-Control-Request-Method
  • Access-Control-Request-Headers
  • Origin

3つのHTTPリクエストヘッダーです。

足りなかったのはAccess-Control-Allow-Headersです。こちらを指定してあげるとよさそうです。

request.php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept");
header("Content-type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
...

第2劇「なんで値がとれないんだよぉ!!」

CORSエラーは無くなりました。ですが肝心の値が取得できていませんでした。
冷静に考えて、さきほどの単純リクエストの説明の際に引っかかる部分がありました。ここです。

Content-Type ヘッダーで指定できるメディア種別に許されるタイプ/サブタイプの組み合わせは、以下のもののみです。
application/x-www-form-urlencoded
multipart/form-data
text/plain

はい、見事にapplication/jsonは入っていませんでした。お疲れ様です。

file_get_contents

file_get_contentsを使用した方法がありました。

request.php
...
$raw = file_get_contents('php://input');
$data = json_decode($raw);
// いろいろ操作する
echo json_encode($data);

php://inputってなんぞや、と思い調べてみると以下のように書かれています。

php://input は読み込み専用のストリームで、 リクエストの body 部から生のデータを読み込むことができます。
https://www.php.net/manual/ja/wrappers.php.php#wrappers.php.input

つまりPOSTで投げられたデータを読み込みを許可する的な意味合いですね。

終劇「でもやっぱり$_POSTで欲しいんだよぉ!!!」

でもやっぱり$_POSTでデータを取得したい、そんな時ありますよね。
大丈夫です。突破口あります。

new URLSearchParams

application/x-www-form-urlencodedでデータ送信すればよさそうです。
フロント側でnew URLSearchParamsを使用します。

main.js
const postData = {
  name: 'john'
}

fetch('http://localhost/request.php', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: new URLSearchParams(postData).toString()
})
  .then(response => response.json())
  .then(res => {
    console.log('🍔', res) // john
  })
  .catch(error => {
    console.error('🍤', error)
  })

バックエンド側は以下の通りです。
Access-Control-Allow-Methods, Access-Control-Allow-HeadersPreflight requestにて利用できるものなので不要になります。

request.php
header("Access-Control-Allow-Origin: *");

$name = $_POST["name"];

echo json_encode($name);

new FormData

multipart/form-dataでデータ送信する方法があります。

main.js
const formData = new FormData()
formData.append('name', 'john')

fetch('http://localhost/request.php', {
  method: 'POST',
  body: formData
})
  .then(response => response.json())
  .then(res => {
    console.log('🍔', res) // john
  })
  .catch(error => {
    console.error('🍤', error)
  })
request.php
header("Access-Control-Allow-Origin: *");

echo json_encode($_POST["name"]);

お気づきでしょうか。ヘッダーにmultipart/form-dataが設定されていません。
この理由ですが、以下の記事が丁寧に解説してくれています。
https://qiita.com/YOCKOW/items/0b9635c62840998708f7

最後に

こんなの当たり前だろ?って声が聞こえてきそうですが、私はこれで3時間ほど無駄にしました。
どうか皆さんは同じ道をたどらないように気をつけてください。

Discussion

ログインするとコメントできます