疑問
なぜ JavaScript の Fetch API はわざわざ fetch(...)
で直接JSONなどのレスポンスのPromiseを返さずに、 わざわざ2回awaitさせてJSONを返すのでしょうか?
非同期構文の助けがあるとはいえ、いちいちPromiseにされると面倒です。
// なぜ await を2回も呼ぶ必要があるのか?
const json = await (await fetch("/api/v1/foobar")).json();
結論からいうと、サーバがレスポンスのヘッダ部を返した段階で即時に処理(継続)を再開できるようにするためです。
たとえば、下記のコードにおいて、サーバ側でレスポンス用のHTTPのヘッダをフラッシュした段階で console.log("サーバが少なくともヘッダを返した");
が実行されるということですね。
const res = await fetch("/api/v1/foobar");
console.log("サーバが少なくともヘッダを返した");
const json = await res.json();
console.log("サーバがボディ(JSON)も全部返した");
Node.js で実験してみる
本当にそうなっているのでしょうか? 実験してみましょう。 (Node.js v18.4.0
)
// $ node server.js
// # => http://localhost:18080
const http = require("http");
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
http
.createServer(async (req, res) => {
// リクエストを受け取って2秒スリープ
await sleep(2000);
// ヘッダを書き出して2秒スリープ
res.setHeader("X-MyHeader-Value", "MY HEADER!!!!");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Expose-Headers", "*");
res.flushHeaders();
await sleep(2000);
// ボディを書き出す
res.write(
JSON.stringify({
message: "Hello delayed message!",
})
);
res.end();
})
.listen(18080);
const task = async () => {
const req = fetch("http://localhost:18080");
console.log(Date(), "Request");
const res = await req;
console.log(Date(), "Headers", JSON.stringify([...res.headers.entries()]));
const json = await res.json();
console.log(Date(), "Body", json);
};
task();
$ node client.js
Mon Dec 05 2022 16:58:54 GMT+0900 (日本標準時) Request
Mon Dec 05 2022 16:58:56 GMT+0900 (日本標準時) Headers [["access-control-allow-origin","*"],["access-control-expose-headers","*"],["connection","keep-alive"],["date","Mon, 05 Dec 2022 07:58:56 GMT"],["keep-alive","timeout=5"],["transfer-encoding","chunked"],["x-myheader-value","MY HEADER!!!!"]]
Mon Dec 05 2022 16:58:58 GMT+0900 (日本標準時) Body { message: 'Hello delayed message!' }
Node.js で fetch を動かしたところ、たしかに下記のようになっているので、
-
16:58:54
: リクエストを発した時刻 -
16:58:56
: ヘッダを受け取った時刻 -
16:58:58
: JSONを受け取った時刻
きちんとヘッダをフラッシュした段階で最初のPromiseが解決され、2秒後のボディの書き込みで続くJSON化の処理が動いていることがわかりました。
ブラウザで実験してみる
ブラウザ (Google Chrome 108
) でも同様の動きになるか試してみます。すでにサーバーのコードでCORSが有効になっているので同様のクライアントコードを動かしてみます。
Mon Dec 05 2022 16:58:07 GMT+0900 (日本標準時) Request
Mon Dec 05 2022 16:58:09 GMT+0900 (日本標準時) Headers [["access-control-allow-origin","*"],["access-control-expose-headers","*"],["connection","keep-alive"],["date","Mon, 05 Dec 2022 07:58:09 GMT"],["keep-alive","timeout=5"],["transfer-encoding","chunked"],["x-myheader-value","MY HEADER!!!!"]]
Mon Dec 05 2022 16:58:11 GMT+0900 (日本標準時) Body {message: 'Hello delayed message!'}
きちんと2秒差でJSONの内容が表示されました。
まとめ
Fetch APIの await
はヘッダがフラッシュされた段階で一旦抜ける柔軟な仕様ということがわかりました。