55
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Fetch API で x-www-form-urlencoded を直接指定することを避ける

Last updated at Posted at 2017-09-07

よくある失敗

Fetch API の使い始めた人のブログ記事を読むと key1=value1&key2=value2 のようなメッセージボディを POST メソッドで送信しようとするものの、Content-Type の値に application/x-www-form-urlencoded を指定することを忘れて、期待通りの投稿内容を得られないという報告がよく見られます。

client.js
const url = "http://httpbin.org/post";
const opt = {
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded"
  },
  body: "key1=value1&key2=value2"
}

fetch(url, opt)
  .then(res => res.text())
  .then(text => console.log(text))
  .catch(err => console.log(err))

対策は FormDataURLSearchParams などの送信のための専用のオブジェクトを使うことです。これらのオブジェクトを使うことで、送信のために必要なヘッダーやエンコーディング方法について悩まなくてすみます。逆にこれらのオブジェクトを使わない場合、HTTP の仕様に関する知識が必要になるので、難易度が上がります。

Fetch のポリフィル

fetch は IE 以外の主要なブラウザーがサポートしています。Node.js でコードを書くに当たり、node-fetch の2系を導入しました。1系は URLSearchParams に対応していません。

yarn add node-fetch@next

FormData

multipart/form-data の形式で投稿したい場合、FormData オブジェクトを使います。主要なブラウザーはサポートしていますが、Node.js の場合、サポートしていないので、form-data パッケージを導入する必要があります。

client.js
const fetch = require('node-fetch');
const FormData = require('form-data');

function createFormData(data) {
  const form = new FormData();
  Object
    .keys(data)
    .forEach(key => form.append(key, data[key]));
  return form;
}

const url = "http://httpbin.org/post";
const data = {
  "key1": "value1",
  "key2": "value2"
};
const form = createFormData(data);
const opt = { method: "POST", body: form }

fetch(url, opt)
  .then(res => res.text())
  .then(text => console.log(text))
  .catch(err => console.log(err));

URLSearchParams

データを application/x-www-form-urlencoded の形式で送信したい場合、URLSearchParams を使います。Chrome、Firefox はサポートするものの、そのほかの主要なブラウザーはサポートしないので、ポリフィルを導入する必要があります。Node.js は v7.0 から URLSearchParams をサポートします。

client.js
const fetch = require("node-fetch");
const { URLSearchParams } = require("url");

function createURLSearchParams(data) {
  const params = new URLSearchParams();
  Object.keys(data).forEach(key => params.append(key, data[key]));
  return params;
}

const url = "http://httpbin.org/post";
const data = {
  "key1": "value1",
  "key2": "value2"
};
const params = createURLSearchParams(data);
const opt = {
  method: "POST",
  body: params
};

fetch(url, opt)
  .then(res => res.text())
  .then(text => console.log(text))
  .catch(err => console.log(err));

パーセントエンコーディングされた結果の文字列がほしいのであれば、toString を使います。何らかの理由でメッセージボディに文字列として指定する必要がある場合、次のように書くことができます。

const body = params.toString();
const url = "http://httpbin.org/post";
const opt = {
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
    "Content-Length": body.length
  },
  body: body
};

FormData と URLSearchParams の使いわけ

FormData (multipart/form-data) は主要なブラウザーがサポートしているのに対して URLSearchParams (application/x-www-form-urlencoded) はサポートしていないブラウザーがあることから、現時点では OAuth のような application/x-www-form-urlencoded を必須とする Web API を使う必要がなければ、FormData を優先することになるでしょう。

multipart/form-data 形式の場合、バウンダリ文字列 (Content-Type": "multipart/form-data; boundary=----WebKitFormBoundarycWUZjDavX3VVIWCe ) が必要になるので、少しでも HTTP メッセージのデータ量を減らしたいのであれば、application/x-www-form-urlencoded を選ぶとよいかもしれません。

その他

PHP の $_POST と ストリーム

PHP の $_POST を使う場合、Content-Typeapplication/x-www-form-urlencoded もしくは multipart/form-data の値を指定する必要があります。multipart/form-data 以外の application/json のような HTTP メッセージのボディの値を求めるのであれば、入力専門のストリーム (php://input) を使います。

$content = file_get_contents("php://input");

async/await の対応

ECMAScript 2017 2017 で Promise のコードを読みやすくするための async/await 構文が導入されました。IE 以外の主要なブラウザーがサポートしています。Node.js は v7.6 から正式に利用できるようになりました。それ以前のバージョンでは --harmony-async-await フラグを指定する必要があります。

async/await を使って fetch のコードを書くと次のようになります。

async function run() {
  try {
    const text = await fetch(url, opt).then(res => res.text());
    console.log(text);
  } catch (e) {
    console.log(e);
  }
}

run();

try/catch 構文を使わずに、await の後で catch を書くこともできます。

async function run() {
  const request = fetch(url, opt).then(res => res.text());
  const text = await request.catch(err => console.log(err));
  console.log(text);
}

run();

もしくは catchthen よりも先に呼び出すこともできます。

async function run() {
  const request = fetch(url, opt)
    .catch(err => console.log(err));
  const text = await request.then(res => res.text());

  console.log(text);
}

run();

try/catchawait/catch の使いわけについてはこちらの記事が参考になります。

Object.entries

ES2017 で Object.entries が導入され、プレーンオブジェクトの要素を for-of (ES2015) ループで展開しやすくなります。IE 以外の主要ブラウザーは導入済みでスニペットやポリフィルも公開されています。

function createFormData(data) {
  const form = new FormData();
  for (let [key, value] of Object.entries(data)) {
    form.append(key, value);
  }
  
  return form;
 }

 const data = { key1: "value1", key2: "value2" };
 const form = createFormData(data);

 for(let [key, value] of form) {
   console.log(key, value);
 }

FormDataURLSearchParams もそれぞれ entrieskeysvalues をもっています。

スペースの扱い

スペースをプラス記号 (+) か パーセントエンコーディング (%20) に置き換えるかで問題になることがあります。いくつかの言語のライブラリについて調べたことは別の記事に投稿しました。

55
38
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?