- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
前回、バイト列をパースしてハンドラを選ぶところまで見ました。今回はその先、返した値がどうバイト列に戻り接続へ書き出されるかです。
まず結論
フレームワークに書く `return "hello"` は、最後には必ず接続への書き出しになります。第13週『システムコール — アプリとカーネルの境界線』で見たとおり、アプリは自分で線にバイトを流せず、書き出しはカーネルへの依頼です。
そして書き終えるまで、応答は存在しません。`res.end()` を呼ばないハンドラは、実行されているのにクライアントへ1バイトも届きません。エラーも出ず、ただ待たされます。
例外を catch して `console.error` だけを実行し、`res` には何も書かないハンドラ。そのURLにブラウザからアクセスすると、何が起きるでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
ハンドラは、応答を「返す」のではなく「材料を渡す」
前回、バイト列をパースしてどのハンドラを呼ぶかが決まるところまで見ました。ここから先は、ハンドラが作ったものが相手に届くまでの話です。
「この関数が応答を返している」という絵を思い浮かべがちですが、正確ではありません。ハンドラがしているのは、材料を組み立ててサーバ側に渡すことだけです。材料は3つ、ステータス・ヘッダ・本文です。
RFC 9112 の §2.1 が、HTTPメッセージの形をこう定義しています。
HTTP-message = start-line CRLF*( field-line CRLF )CRLF[ message-body ]
応答も同じ形です。最終的に線を流れるのはこの形に組み上がった1本のバイト列であって、オブジェクトでも構造体でもありません。
そして書き出しそのものは、アプリにはできません。第13週『システムコール — アプリとカーネルの境界線』で見たとおり、アプリはユーザーモードで動いていてハードウェアに直接触れません。バイトを線に流すのはカーネルへの依頼です。
hello world の各行が、何に相当するか
Node 標準の http で最小のサーバを書くと、こうなります。
const http = require("http");const server = http.createServer((req, res) => { res.end("hello"); });server.listen(3999, () => { console.log("listening", JSON.stringify(server.address())); });
この3行が、接続を受ける一連の呼び出しのどこに相当するか並べます。
コードの部分 相当する呼び出し 何が起きるかhttp.createServer(fn) —— fn を覚えるだけserver.listen(3999) socket / bind / listen 端点を作り、ポートを割り当て、受動にする(接続が来る) accept 新しい fd が返る(リクエストが届く) read バイト列が読まれ、fn が呼ばれるres.end("hello") write ステータス行・ヘッダ・本文が書き出される(keep-alive が切れる) close 接続が閉じられる
この表は strace や dtruss で採取した出力ではありません(手元の macOS では採取していません)。実際にはもっと多くの呼び出しが挟まります。
注目すべきは最後から2行目です。res.end("hello") という1行だけが、書き出しに対応しています。 createServer に渡した関数は、呼ばれるだけでは何も書き出しません。
Node のドキュメントは response.end() をこう説明しています。
This method signals to the server that all of the response headers and body have been sent; that server should consider this message complete. The method,
response.end(), MUST be called on each response.
すべての応答で呼ばれなければならない(MUST)。返ってくるバイト列はこうです。
HTTP/1.1 200 OKDate: Wed, 16 Sep 2026 04:11:10 GMTConnection: keep-aliveKeep-Alive: timeout=5Content-Length: 5hello
ヘッダは4つだけで、Content-Type は付きません。付けと言っていないものは付かないのです。
Node の `http.createServer((req, res) => { res.end("hello") })` で、接続へバイトを書き出しているのはどの部分ですか。
書き終えるまで、応答は存在しない
ここから一般化します。「ハンドラが実行された」と「応答が返った」は、別の出来事です。
同じものだと思っていると、次の状況が読めません。ログにはリクエストが記録され console.log も出ているのに、ブラウザは待ち続けている。書き出しに対応する行が実行されていなければ、線には1バイトも流れていないからです。
console.log だけをするハンドラのサーバに、curl -m 5 を投げました。
* Trying 127.0.0.1:3998...* Connected to 127.0.0.1 (127.0.0.1) port 3998> GET / HTTP/1.1> Host: 127.0.0.1:3998> User-Agent: curl/8.7.1> Accept: */*>* Request completely sent off* Operation timed out after 5002 milliseconds with 0 bytes received* Closing connectioncurl: (28) Operation timed out after 5002 milliseconds with 0 bytes received[curl exit 28]### server stdout after curl:request received: GET / 2026-09-16T04:11:18.066Z
最後の行を見てください。ハンドラは実行されています。それでも 0 bytes received で、諦めたのは curl 側です。Node が自分で打ち切ることはないのでしょうか。70秒まで延ばしました。
curl: (28) Operation timed out after 70003 milliseconds with 0 bytes receivedhttp=000 time_total=70.003195[curl exit 28]
70秒経っても切られません。設定の実値を出力させると、こうです。
defaults: requestTimeout=300000 headersTimeout=60000 keepAliveTimeout=5000 timeout=0
headersTimeout は60秒ですが、70秒待っても効きません。ドキュメントに理由があります。
Limit the amount of time the parser will wait to receive the complete HTTP headers.
「ヘッダを完全に受信するのを待つ時間」の上限です。この実験ではヘッダは既に全部届いています(curl の Request completely sent off がその証拠)。受信の待ち時間なので効きません。
戻り値で返す設計 — WSGI と Rack
res に書く形が唯一の作法ではありません。同じことを関数の戻り値で表す設計もあります。PEP 3333 の最小のアプリです。
HELLO_WORLD = b"Hello world!\n"def simple_app(environ, start_response):"""Simplest possible application object"""status = '200 OK'response_headers = [('Content-type', 'text/plain')]start_response(status, response_headers)return [HELLO_WORLD]
start_response でステータスとヘッダを渡し、本文は return [HELLO_WORLD] で返します。仕様はこう規定しています。
When called by the server, the application object must return an iterable yielding zero or more bytestrings.
本文は「0個以上のバイト列を yield するイテラブル」。Rack はより短くまとめます。
A Rack application is a Ruby object that responds to +call+. It takes exactly one argument, the +environment+ (representing an HTTP request) and returns a non-frozen +Array+ of exactly three elements: the +status+, the +headers+, and the +body+ (representing an HTTP response).
戻り値は [status, headers, body] の3要素配列。先の材料3つが、そのまま3要素になっています。
どちらの設計でも、書き出しているのはアプリではありません。 材料を RFC 9112 の形に組み上げて接続へ書き出すのはサーバ側です。
この違いが、応答を取りこぼす経路の違いになります。戻り値で返す設計では、関数が値を返せば材料は渡ります。res に書く設計では、書く呼び出しを実行しないまま抜けられてしまう。上の実機がその状態です。
WSGI(PEP 3333)と Rack のアプリは、応答をどうやってサーバ側へ渡しますか。
res オブジェクトに書く(Node の http)
ハンドラは `res` を受け取り、そこに書きます。戻り値は使われません。`res.end()` が完結の合図で、この呼び出しがあって初めてバイト列が書き出されます。
材料 Node の http WSGI / Rackステータス res.writeHead start_response の第1引数 / 配列の第1要素ヘッダ res.setHeader / writeHead start_response の第2引数 / 配列の第2要素本文 res.write / res.end イテラブル / 配列の第3要素完結の合図 res.end() 関数が値を返したこと
戻り値で返す(WSGI と Rack)
アプリは呼び出し可能なオブジェクトで、材料を戻り値(と WSGI では `start_response`)でサーバへ渡します。書き出すのはサーバ側です。上の表の右列がその対応です。
`res` に書く設計と、戻り値で返す設計。書き方はまるで違いますが、両者が同じことをしていると言えるのはなぜでしょうか。
「この関数が終わったあと、サーバ側の手元に何があるか」を両方で考えてみてください。
図で見る — return から線まで
- ・3段目が作られなければ、4段目から下は一切起きない
- ・ログに残るのは1段目まで。相手が見ているのは6段目
要点は、1段目と6段目の間に4つの段があることです。両者がずれるのは異常ではなく、間のどこかが進んでいないだけです。
この章のまとめ
- ハンドラが組み立てるのはステータス・ヘッダ・本文の3材料。線に流すのはカーネルへの依頼(第13週)。
- 材料は RFC 9112 §2.1 の形の1本のバイト列に直列化される。この形になって初めて応答が存在する。
- Node の
res.end()は完結の合図。ドキュメントは "The method,response.end(), MUST be called on each response." と書いている。 - 「ハンドラが実行された」と「応答が返った」は別の出来事。
res.end()を呼ばないと、ログは出るのに0 bytes receivedで exit 28。70秒でも同じ。 headersTimeout(既定 60000)はヘッダの受信を待つ時間なので、この状況に効かない。- WSGI は
start_response+ イテラブル、Rack は[status, headers, body]。作法は違うがバイト列は同じ。
この先の学びにどう繋がるか
次回は「本文」の中身がどう作られるかを見ます。テンプレートも JSX も、結局は文字列を組み立てているだけです。
関連する既存クイズは「サーバーのしくみ V(リバースプロキシとアプリ連携)」と「サーバーのしくみ III(並行処理モデル)」です。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
「`res.end()` を呼び忘れると Node が既定のタイムアウトで打ち切り 408 を返す」という説明を検証したい。手元の実測で確かめられたのはどれですか。
用語集(5語)
- ハンドラ
- 応答の材料を組み立てる関数。線に流すのはハンドラではなく外側のサーバ側コード。
- response.end()
- Node の `http` で応答を完結させるメソッド。すべての応答で呼ばれねばならない(MUST)。
- WSGI (PEP 3333)
- Python の取り決め。アプリは `(environ, start_response)` を受け、本文はバイト列を yield するイテラブルとして返す。
- Rack
- Ruby の取り決め。アプリは `call` に応答し、ステータス・ヘッダ・本文の3要素配列を返す。
- headersTimeout / requestTimeout
- Node の `http.Server` の設定。どちらも受信の待ち時間の上限で、既定は 60000 / 300000。