- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
前回、listen したプロセスがバイト列を受け取るところまで見ました。今回は受け取った後に何が起きるかです。構造体は流れてきません。
まず結論
サーバに「リクエスト」という構造体が届くことはありません。届くのはバイト列で、CRLF で区切って開始行を読み切って初めてメソッドとパスが確定します。
ハンドラ表を引けるのはその後です。そして「どのハンドラを選ぶか」の規則は規格が定めておらず、実装ごとに違います。Express は定義順、nginx は最長前置一致でした。
Express で `app.get('/users/:id', ...)` を書き、その**後ろに** `app.get('/users/new', ...)` を書きました。`/users/new` へのリクエストはどちらのハンドラに届くでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
構造体は流れてこない
ハンドラの引数には req.method や req.path が入っています。ここから「リクエストというまとまった物がそのまま運ばれてくる」と考えてしまいがちです。
そうではありません。 流れてくるのは区切りの書かれていないバイト列です。導線『接続して送る — TCP接続の上を流れるHTTPリクエストのバイト列』で見たとおり、送る側もバイト列を書き込んでいるだけでした。切り分け方を決めているのは RFC 9112 §2.1 です。
HTTP-message = start-line CRLF *( field-line CRLF ) CRLF [ message-body ]
同じ節は、ヘッダ部の終わりを示すものを “an empty line indicating the end of the header section”(空行)と述べています。
ここから順序が読み取れます。メソッドとパスが確定するのは、開始行を CRLF まで読み切った時点です(§3 の request-line は method SP request-target SP HTTP-version)。そして本文の範囲は、ヘッダを読み終えるまで決まりません。 本文長を決める Content-Length 自体がヘッダだからです。
バイト列を手で組み立てて送ってみる
実機(macOS、express 5.2.1、Node v24.21.0)で printf が作ったバイト列を nc に流し込みました。ブラウザも curl も使いません。
$ printf 'GET /a HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n' | nc localhost 3997HTTP/1.1 404 Not FoundX-Powered-By: ExpressContent-Security-Policy: default-src 'none'X-Content-Type-Options: nosniffContent-Type: text/html; charset=utf-8Content-Length: 140Date: Wed, 16 Sep 2026 04:11:26 GMTConnection: close<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Error</title></head><body><pre>Cannot GET /a</pre></body></html>
送ったのは開始行・ヘッダ2行・空行だけで、サーバは 404 を返しました。特別な形式は何もありません。
本文の Cannot GET /a に注目してください。サーバはバイト列から GET と /a を取り出し、そのうえで表を引いて該当なしと判定しています。パースが先、表引きが後がこの応答文に現れています。
RFC 9112 の形式で、ヘッダ部が終わったことを示すものは何ですか。
規格が決めているのはどこまでか
RFC 9112 が定めているのはバイト列の構文です。開始行の形、ヘッダの区切り、終端、そして本文長の判定。§6.3 は “The length of a message body is determined by one of the following (in order of precedence):” と書き、優先順位つきの項目を並べます。項目3はこうです。
If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length.
同じ項目は、そういうメッセージが request smuggling や response splitting の試みでありうるため “ought to be handled as an error” とも述べます。リクエストでどれにも当てはまらなければ本文長は 0 です(項目7)。
そして規格はここで終わります。 取り出した request-target をどのハンドラに結びつけるか——「ルーティング」と呼ばれる部分——を、RFC 9112 は定めていません。
だから表の引き方は実装ごとに違います。「ルーティングは最長一致」も「上から順」も、どの実装の話かを言わない限り成り立ちません。第18週『ルーティング表 — 複数の行が当てはまったら、どれを選ぶのか』で、ルータの表とファイアウォールの表が同じ形をしながら別の規則で引かれることを見ました。同じ混同がここでも起きます。
- ・本文長の判定がヘッダの後なのは、判定材料の Content-Length 自体がヘッダだから
- ・最後の段だけが実装ごとに違う
順序が結果を変える — Express の実機
規定の外にある最後の段を実物で見ます。/users/:id を先に、/users/new を後に定義したサーバです。
app.get("/users/:id", (req, res) => { res.send("id route: id=" + JSON.stringify(req.params.id)); });app.get("/users/new", (req, res) => { res.send("new route"); });$ curl -s http://127.0.0.1:3997/users/newid route: id="new"$ curl -s http://127.0.0.1:3997/users/42id route: id="42"
/users/new は :id のルートに食われ、id が "new" になりました。後ろの専用ルートには届いていません。定義を入れ替えると結果が変わります。
$ curl -s http://127.0.0.1:3996/users/newnew route$ curl -s http://127.0.0.1:3996/users/42id route: id="42"
結果を変えたのはパスの具体性ではなく、書いた順序です。 /users/42 はどちらの順序でも :id に届きます。変わるのは /users/new だけです。
実機で `/users/:id` を先に、`/users/new` を後に定義したとき、`/users/new` と `/users/42` はどこへ届きましたか。
Express のルート — 記述順で決まる
同じ `/users/new` に対し、定義の順序だけを入れ替えると応答が変わりました。`:id` はリテラルの `new` にも一致します。「完全一致が優先される」という規則は、この実装には見当たりません。
:id を先に定義 → GET /users/new → id route: id="new"/users/new を先 → GET /users/new → new routeどちらの順序でも → GET /users/42 → id route: id="42"
nginx の prefix location — 最長一致で決まる(第31週)
prefix location は設定ファイルのどこに書いてあっても、最も長く一致したものが選ばれます。第31週で見たとおり、その最長一致は selected and remembered — 暫定候補にすぎず、後の正規表現に逆転されえます。
段 何をするか 順序は効くか① prefix を全部見て最長一致を「記憶」 効かない(長さで決まる)② 記憶したものが ^~ ならここで確定 —③ 正規表現を設定ファイルの出現順に検査 効く(最初の一致で採用)⑤ どれも一致しなければ①の記憶を採用 —(④ predicate は本筋外のため省略。番号は第31週のまま)
同じ「パスからハンドラを選ぶ」という質問に、Express と nginx は正反対の答え方をします。なぜでしょうか。
RFC 9112 がどこまでを定めていたかを思い出してください。ルート選択の規則は、どの文書に書かれているでしょうか。
この章のまとめ
- 流れてくるのはバイト列で、「リクエスト」という構造体は届かない。
req.methodはライブラリが組み立てた結果。 - RFC 9112 §2.1 の形式は
start-line CRLF *( field-line CRLF ) CRLF [ message-body ]。ヘッダ部の終わりを示すのは空行。 - メソッドとパスが確定するのは開始行を読み切った時点(§3)。本文長が決まるのはヘッダを読み終えた後(§6.3)。判定材料の
Content-Length自体がヘッダなので、順序は逆にできない。 - §6.3 の項目3: 両方あれば Transfer-Encoding が優先し、エラーとして扱われるべきとされる。
- ハンドラ表の引き方は RFC 9112 の規定の外。 ここだけが実装ごとに違う。
- 実機(express 5.2.1)では
/users/:idを先に書くと/users/newはid="new"として食われた。結果を変えたのは記述順。 - ただし公式文書が順序を規定しているのは middleware についてで、route を主語にした「最初の一致で確定」の一文は見当たらない。実機で確かめた観測として扱う。
- nginx の prefix location は最長一致(第31週)、第18週のルータの表も最長一致。「ルーティングとはこう動く」という一般則は作れない。
次はハンドラの中身です。hello world と書いたコードのどの行が socket への書き込みになっているのかを追います。
既存のクイズで手を動かすなら、サーバーのしくみ の「サーバーのしくみ IV(HTTPサーバーの内部動作)」で確かめられます。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
RFC 9112 §6.3 で、`Transfer-Encoding` と `Content-Length` が両方あるメッセージの扱いはどう定められていますか。
用語集(5語)
- 開始行 (start-line)
- メッセージの1行目。RFC 9112 §2.1 では request-line か status-line。request-line は `method SP request-target SP HTTP-version`(§3)。
- CRLF
- 復帰(CR)と改行(LF)の2バイトの並び。HTTP/1.1 は開始行と各ヘッダ行をこれで区切り、ヘッダ部の終わりを空行で示す。
- request-target
- request-line のうちメソッドとバージョンに挟まれた部分。どのハンドラに結びつけるかは RFC 9112 の外。
- 本文長の判定順 (RFC 9112 §6.3)
- 本文の範囲を決める優先順位つきの規則。両方あれば Transfer-Encoding が優先し(項目3)、どれにも当てはまらなければ 0(項目7)。
- パスパラメータ
- `/users/:id` の `:id` のように、パスの一区間を変数として受け取る書き方。何に一致するかは実装が決め、`new` のようなリテラルにも一致しうる。