よくある失敗
Fetch API の使い始めた人のブログ記事を読むと key1=value1&key2=value2
のようなメッセージボディを POST メソッドで送信しようとするものの、Content-Type
の値に application/x-www-form-urlencoded
を指定することを忘れて、期待通りの投稿内容を得られないという報告がよく見られます。
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
に対応していません。
yarn add node-fetch@next
FormData
multipart/form-data
の形式で投稿したい場合、FormData オブジェクトを使います。主要なブラウザーはサポートしていますが、Node.js の場合、サポートしていないので、form-data パッケージを導入する必要があります。
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
をサポートします。
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-Type
に application/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();
もしくは catch
を then
よりも先に呼び出すこともできます。
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
の使いわけについてはこちらの記事が参考になります。
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);
}
FormData
と URLSearchParams
もそれぞれ entries
、keys
、values
をもっています。
スペースの扱い
スペースをプラス記号 (+
) か パーセントエンコーディング (%20
) に置き換えるかで問題になることがあります。いくつかの言語のライブラリについて調べたことは別の記事に投稿しました。