- ブラウザURL入力今ここ
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理
- DB保存
まだ何も送っていません。打ち込まれた文字列がブラウザの中でどう分解されるかだけを見ます。
まず結論
アドレスバーに `http://localhost:80/` と入れても、残るのは `http://localhost/` です。`:80` は「省略できる」のではなく、パーサが既定ポートと同じ値を null に書き換えるためです。
URL パーサは最初に scheme を決め、それが特別扱いのスキームなら既定ポートを持ちます。http の既定は 80 なので `:80` は消え、`:3000` は既定と違うので残ります。
この段階ではまだ何も送っていません。文字列の分解だけの話です。
アドレスバーに `http://localhost:80/` と入力してページを開いたあと、アドレスバーに残る文字列はどれでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
パーサは、まず scheme を決める
前のレッスン『何を追うのか — ファイルが「表示」に変わる7つの継ぎ目』で継ぎ目を並べました。今回はその最初の1つを見ます。
この段階ではまだ1バイトも送っていません。 通信も名前解決も始まっておらず、起きているのは文字列の分解だけです。
URL パーサが最初に確定させるのが scheme です。WHATWG URL Standard の手順では、先頭が ASCII 英字なら scheme を読む状態に入り、英数字と + - . を溜めていき、: で決まります。
次に効くのが、その scheme が special scheme(特別扱いのスキーム) かどうかです。規格の表に載ったスキームだけが既定ポートを持ちます。
port を読む段では、次の一文が効きます。
「省略できる」ではありません。「書いても null にされる」です。 入力に :80 があってもパースの結果として port は null になり、だから表示にも残りません。
実際に分解させてみる
Node v24.21.0 の new URL() に4つの文字列を渡した実測です。
http://localhost:80/ => {"protocol":"http:","host":"localhost","port":"","pathname":"/","href":"http://localhost/"}http://localhost:3000/ => {"protocol":"http:","host":"localhost:3000","port":"3000","pathname":"/","href":"http://localhost:3000/"}https://example.com:443/ => {"protocol":"https:","host":"example.com","port":"","pathname":"/","href":"https://example.com/"}localhost:3000/api => {"protocol":"localhost:","host":"","port":"","pathname":"3000/api","href":"localhost:3000/api"}
1行目、:80 は消えて port が空文字、href は http://localhost/ になりました。規格の null は JavaScript の URL.port では空文字として現れます。3行目の :443 も https の既定ポートなので消えます。
2行目の :3000 は残りました。3000 が既定の 80 と等しくないからです。判定は「既定と同じか」だけで、番号の大小や宛先の種類は見ていません。
Chrome 152 のコンソールでも同じでした(new URL("http://localhost:80/") は port が ""、href が http://localhost/)。どちらも同じ手順を実装しているためです。4行目は後で扱います。
`http://localhost:3000/` の `:3000` が消えずに残るのは、なぜですか。
推奨と正規化は別物
よく混ざる2つの文書を分けておきます。RFC 3986 §3.2.3 にも既定ポートの話がありますが、書き方が違います。
注意点が2つ。ひとつ、この should は小文字で、RFC 2119 のキーワードとして大文字で書かれた SHOULD ではありません。ふたつ、**宛先は URI を作る側(producers)と正規化する側(normalizers)**で、受け取ってパースする側ではありません。
つまり RFC 3986 は「URI を組み立てるなら既定と同じポートは書かないでおくのがよい」という助言、WHATWG は「渡された URL をパースするとき既定と同じ port は null にする」という手順です。助言と手順、書く側と読む側。層が違います。
この区別を落とすと「:80 は RFC で省略が義務づけられている」という説明ができあがります。確かめるべきは、大文字か小文字か、誰に向けた文かです。一方の結論を他方の名前で語ると、内容が合っていても帰属が壊れます。
scheme を書かないと、分解そのものが変わる
さきほどの実測の4行目に戻ります。
localhost:3000/api => {"protocol":"localhost:","host":"","port":"","pathname":"3000/api","href":"localhost:3000/api"}
エラーになっていません。localhost が scheme として読まれています。
手順をたどれば理由がわかります。先頭の l は ASCII 英字なので scheme を読む状態に入り、続く各文字も英数字なので溜まっていき、: の時点で scheme は localhost に確定します。
そこから先は localhost が表に無いことが効きます。特別扱いではないため、残りが / で始まっていれば path or authority state へ進みますが、ここは 3000/api です。規格の分岐はこうです。
結果、3000/api がまるごと path になりました。host も port も無い URL です。Chrome 152 でも同じで、protocol localhost:、host ""、pathname 3000/api でした。
これを使うとどうなるか。Node の fetch に渡した実測です。
fetch error: fetch failedmessage: fetch failedcause: Error: unknown schemeat makeNetworkError (node:internal/deps/undici/undici:11841:35)at schemeFetch (node:internal/deps/undici/undici:13401:34)at mainFetch (node:internal/deps/undici/undici:13242:30)
表面に出るのは fetch failed だけで、原因が unknown scheme だと分かるのは e.cause を開いてからです。接続に失敗したのではなく、送り先を決める段まで到達していません。
`localhost:3000/api` という文字列を URL パーサに渡すと、どう分解されますか。
URL の port は scheme が決まってから読まれる
パーサは port の数字を読み終えても即座には確定させず、scheme の既定ポートと比べます。同じ `:80` でも scheme が http なら消え、http でなければ残ります。どのスキームの下にあるかで扱いが変わります。
入力 既定ポート パース後の porthttp://localhost:80/ 80 null(消える)http://localhost:3000/ 80 3000(残る)https://example.com:443/ 443 null(消える)
接続を識別するポート(第21週)
TCP の接続は両端の IP とポートを合わせた4つの値で区別されます。宛先の 443 番だけでは何千本もが該当し、1本に定まりません。番号は組になって初めて特定の接続を指します。
値 URL に書けるか送信元IP 書けない送信元ポート 書けない宛先IP / ホスト名 書ける(host)宛先ポート 書ける(port)
URL に書く port と、接続を識別するときのポートは、同じものでしょうか。
URL に書ける値と、接続を1本に絞るのに必要な値を、それぞれ並べて数えてみてください。
判定の流れを1枚にする
手順を、port が消えるか残るかの判定に絞って並べます。
- ・`:3000` は最後の段で「等しくない」となり、そのまま残る
- ・`localhost:3000/api` は2段目で「表に無い」となり、host も port も持たない形になる
順序が効いています。scheme が先に決まらないと、既定ポートが何かも決まりません。 だから http:// を落とすと、port の判定どころか、どこが host かという分解自体が変わります。
なお port を区別するかどうかは、この先も形を変えて出てきます。ブラウザのプロセス分離には origin と site という単位があり、origin は port を区別しますが site は無視します。ブラウザ編『分離の単位は origin ではなく site — 妥協が設計に残っている』で扱います。
この章のまとめ
- URL パーサは先頭から読んで最初の
:で scheme を確定させる。ここが決まらないと後段の判定が始まらない。 - 表に載ったスキームだけが既定ポートを持つ(http 80 / https 443 / ftp 21 / ws 80 / wss 443 / file は null)。表に無い文字列の既定ポートは null。
- port は「既定と等しければ null、そうでなければその値」。だから
:80は「省略できる」のではなく「書いても消える」。new URL("http://localhost:80/").portは空文字。 - RFC 3986 §3.2.3 も既定ポートに触れるが、小文字の should omit で、宛先は URI を作る側と正規化する側。パーサへの規定ではない。
http://を落とすと分解が変わる。localhost:3000/apiは schemelocalhost:+ path3000/apiで host も port も無い。Node の fetch はunknown schemeで失敗。- URL が表現するのは宛先側の host と port だけ。残りは後の段で OS が埋める。
この先の学びにどう繋がるか
次のレッスン『名前がアドレスになる — localhost は DNS に行かない』では、ここで分解した host がどうアドレスに変わるかを見ます。localhost が URL の層で特別扱いされなかった以上、では誰が扱うのか、という問いが立ちます。
ポート番号の区分(System / User / Dynamic)と接続を識別する4つの値は第21週『ポートという多重化 — 1本の線に何本もの会話』で扱います。用語は既存のクイズ群「HTTP用語(単語帳)」「TCP用語(単語帳)」が対応します。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
WHATWG URL Standard が挙げる特別扱いのスキーム(special scheme)と既定ポートの対応として、正しいものはどれですか。
用語集(5語)
- scheme(スキーム)
- URL の先頭、最初の `:` までの部分。`http`、`https`、`file` など。パーサは先頭から順に読み、`:` でここを確定させる。
- special scheme(特別扱いのスキーム)
- WHATWG URL Standard が表で挙げるスキームの集合。ftp / file / http / https / ws / wss。2列目に既定ポートが並ぶ。表に無い文字列の既定ポートは null。
- 既定ポート(default port)
- そのスキームに割り当てられたポート番号。http は 80、https は 443。パース時に port がこれと等しければ url の port は null に置き換えられる。
- opaque path
- 特別扱いでないスキームで `:` の次が `/` で始まらないとき、残り全体が path に入る状態。`localhost:3000/api` がこれで、host も port も持たない。
- 正規化(normalization)
- 同じ意味の URL を1つの形に揃えること。既定ポートを null にするのが一例。入力どおり保存するのではなく、決められた形に書き換える。