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


## よくある失敗

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

```js: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))
```

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

## Fetch のポリフィル

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

```bash
yarn add node-fetch@next
```

## FormData


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

```js: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` をサポートします。

```js: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` を使います。何らかの理由でメッセージボディに文字列として指定する必要がある場合、次のように書くことができます。

```js
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-Type` に `application/x-www-form-urlencoded` もしくは `multipart/form-data` の値を指定する必要があります。`multipart/form-data` 以外の `application/json` のような HTTP メッセージのボディの値を求めるのであれば、入力専門のストリーム (`php://input`) を使います。

```php
$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 のコードを書くと次のようになります。

```js
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 を書くこともできます。

```js
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();
```

もしくは `catch` を `then` よりも先に呼び出すこともできます。

```js
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/catch` か `await/catch` の使いわけについては[こちら](http://qiita.com/akameco/items/cc73afcdb5ac5d0774bc)の記事が参考になります。

### Object.entries

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

```js
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);
 }
```

`FormData` と `URLSearchParams` もそれぞれ `entries`、`keys`、`values` をもっています。


### スペースの扱い

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