Denoのhttpモジュールの問題点
結論
Denoのhttpモジュールはまだ全然production readyじゃない
なのでhttpサーバを使いたかったらservest by keroxp.iconを使いましょう 標準モジュールよりも進んでいてバグも少ないです
あらまし
2019/10/8現在のDenoのhttpモジュールにある未解決の問題点を解説しますkeroxp.icon
一応断っておくと、今現在も僕はDeno本体とdeno_std両方のコントリビュータです
最初にRyanが作ってその後多くの人がメンテナンスに参加した感じ
で、denoのhttpモジュールにはいくつかの未実装機能や既知のクリティカルなバグがあります
なのでdenoの標準モジュールのhttpもそれを使っている3rd partyのhttpフレームワークもproduction readyではないです
問題点1: Keep-Alive コネクションを正しく処理していない
というかこれが全てというか
DenoのhttpはHTTP/1.1のKeep-Aliveコネクションを正しく処理できません
code:ts
async function main() {
let i = 0;
const ok = new Uint8Array(1,2) for await (const req of serve("0.0.0.0:8888")) {
// 2回に一回だけbodyを読み出すエコーサーバ
if (i % 2 === 0) {
const body = await req.body();
req.respond({ status: 200, body });
} else {
req.respond({ status: 200, body: ok})
}
i++;
}
}
main();
このコードがわかりやすい
このサーバーはPOSTリクエストを受けたとき2回に1回bodyを読み出してそれをそのまま返す実装
Keep-Aliveコネクションを貼ったhttpエージェントがこのサーバに3回POSTリクエストを投げると、3回めのPOSTは失敗します
そもそもKeep-AliveコネクションはTCPセッションの確立コストを省くために、同じホストに対しては一つのTCPストリームを使いまわしてその中で何回もHTTP/1.1のリクエストを送って読むという方式
なので基本的にはサーバーは一つのKeep-Alvieコネクションの中で
並列にリクエストを処理できない
必ずクライアントから送られたすべてのpayload(status,header,body,trailers)を読み出さないといけない
なぜならbody(とtrailers)の次に次のリクエストのpayloadが来るから
次のリクエストが来るか来ないか分からなくてもread待ちし続けなくてはいけない
ので何もしなければずっとサーバーとクライアントはつながったまま
なのでKeep-Alive Timeoutというクライアントから一定時間リクエストが来なかったらコネクションを切断するという仕様がある
という難しい制約があるのだが、Denoのhttpモジュールはこれらを正しく処理していません
http.serveやhttp.listenAndServeのハンドラーでreq.body()のストリームを読み出さなかった場合、その次のKeep-Aliveコネクションのリクエストを読みだせません
ここで何が起こるかと言うと、読み出さなかった前のリクエストのbodyの部分を次のhttpリクエストのstatus lineだと解釈しようとして失敗するという感じ
これは大問題とかそういう以前の単純なRFC違反なので、全然使えないです
HTTP/1.1のRFC的にはKeep-Aliveコネクションを処理しなくてもいいのだが、その場合はリクエストが処理されたらサーバー側からコネクションを切断しないといけない
つまり現状の実装はどっちつかずの残念な状態になっている
これを回避するためには、ハンドラーを呼び出した後に、
bodyが読み出されたか
読み出されなかったか
読み出されたとしたらどこまで読み出されたのか
ということを把握しておいて、次のリクエストを読み出す前にbodyをconsumeしなくてはいけません
ちなみにこれは既知の問題で、コントリビュータのkevinやbartekが以前PRを出していたのだけど何故か両方ともcloseされてる
問題点2: for awaitを未だに推し続けている
なんかもう解説するのが面倒になってきた…keroxp.icon
問題点3: ベンチマークが不正確
denoのhttp_bench.tsは、wrkというベンチマークソフトを使っている このベンチマークサーバーがベンチマーク用に最適化されすぎて、ほとんど意味がない
code:http_bench.ts
for await (const req of serve(":8888")) {
// awaitしてない
req.respond({status: 200, body})
}
req.respondはkeep-aliveコネクションにレスポンスを書き込むasync関数なのだけど、それをawaitせずに処理してます
awaitをしないおかげで、レスポンスのwrite完了を待たずに次のリクエストを読み出し始めているのですが、これは問題点1で指摘した未読み出しbodyの処理をしていないから可能なものであって、ベンチマークがGETではなくPOSTリクエストの場合は成立しないコードになっています
servestのbenchmarkとの比較だと、大体↑のやつは6倍ほど速いのですが、awaitを入れると同じくらいになります