- ブラウザURL入力今ここ
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口今ここ
- アプリ処理
- DB保存
前回は、HTTP の意味は1つで、運び方が版ごとに違うことを見ました。今回は、まだ存在しない値について両端が何を共有しているかです。
まず結論
通信の仕組みは、両端が何を共有しているかで決まります。この回で両端が共有しているのは値の一覧ではなく、「知らない値が来たらどう扱うか」という規則です。
HTTP のクライアントは知らないステータスコードを同じクラスの x00 として読み、TLS のサーバは知らない拡張を無視します。この規則を先に共有しているから、相手を更新せずに新しい値を足せます。GREASE は、その規則が本当に守られているかを、意味の無い値を送って確かめる仕掛けです。
Chromium 151 を手元の TLS サーバ(OpenSSL 3.6.3 の s_server)に接続させると、サーバの記録の暗号スイートや拡張に UNKNOWN と表示される値が混じっていました。サーバはどうしたでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
共有しているのは、表に無い値の読み方
HTTP のステータスコードは増え続けます。すべてのクライアントが最新の一覧を持っているとは限りません。それでも新しいコードを使えるのは、RFC 9110 §15 が一覧ではなく読み方を共有させているからです。
However, a client MUST understand the class of any status code, as indicated by the first digit, and treat an unrecognized status code as being equivalent to the x00 status code of that class.
分類に使うのは先頭1桁だけです(The last two digits do not have any categorization role)。下2桁の意味が無いのではなく、クラス分けに使わないという意味です。
同じ節は例まで書いています。if a client receives an unrecognized status code of 471, it can see from the first digit that there was something wrong with its request and treat the response as if it had received a 400 (Bad Request) status code。471 は例のための未登録の番号です。
ヘッダも同じ形です。§5.1 は、プロキシは知らないヘッダを転送し(MUST)、それ以外の受け手は無視する(SHOULD)と定め、理由をこう書きます。
Adhering to these requirements allows HTTP's functionality to be extended without updating or removing deployed intermediaries.
- ・両端が共有しているのは値の一覧ではなく、一覧に無い値の扱い
- ・471 は RFC 9110 が例に使った未登録の番号
471 を返すサーバに curl を当てる
パスの数字をそのままステータスにする Node v24.21.0 のサーバです。
const http = require('node:http');
http.createServer((req, res) => {
const code = Number(req.url.slice(1)) || 200;
res.writeHead(code, { 'content-type': 'text/plain' });
res.end(`you asked for ${code}\n`);
}).listen(Number(process.argv[2] || 18082), () => console.log('listening', process.version));
macOS の curl 8.7.1 で、/471 /400 /200 に curl -s --fail -o /dev/null -w "status=%{http_code} " …; echo "exit=$?"、続けて /471 に curl -si と curl -sS --fail を当てました。
status=471 exit=22status=400 exit=22status=200 exit=0HTTP/1.1 471 unknowncontent-type: text/plainDate: Thu, 24 Sep 2026 01:53:39 GMTConnection: keep-aliveKeep-Alive: timeout=5Transfer-Encoding: chunkedyou asked for 471exit=0curl: (22) The requested URL returned error: 471exit=22
471 と 400 が同じ終了コード 22 になりました。curl が 471 を 400 として読んだように見えますが、そうは言えません。手元のマニュアルは終了コード 22 をこう説明します。
The requested URL was not found or returned another error with the HTTP error code being 400 or above.
判定は「400 以上か」という数値のしきい値です。RFC の規則と結果が一致しただけで、クラス規則を実装している証拠にはなりません。なお、Node は知らない番号に理由句 unknown を付けて送りました。
curl 8.7.1 の `--fail` は、471 を返すサーバに対して終了コード 22 を返しました。この結果から言えることはどれですか。
規則は層ごとに書かれ、破られても気づかれない
「知らない値は無視する」は HTTP のコードとヘッダだけの話ではありません。HTTP/2 は RFC 9113 §4.1 で Implementations MUST ignore and discard frames of unknown types.、TLS 1.3 は RFC 9846 §4.2.2 で Servers MUST ignore unrecognized extensions. と書きます。RFC 9846 は2026年7月に出た改訂版で、TLS 1.3 を定めていた RFC 8446 を廃止しました。この一文は 8446 と同文です。
ただし無視してよいのは知らない値です。知っている拡張が、現れてはいけないメッセージに入っていたら、握手を中断します(RFC 9846 §4.3)。規定違反は未知ではありません。
規則は共有されていても、実装が守っているとは限りません。RFC 8701 §1 はこう書きます。
However, bugs may cause an implementation to reject unknown values. It will interoperate with existing peers, so the mistake may spread through the ecosystem unnoticed.
知らない値を拒む実装も、既存の相手とは問題なくつながります。破りが見えるのは、新しい値が実際に定義されて送られたときです。RFC 8701 はこれを、継ぎ目が錆びついた(rusted shut)と呼びます。
落とした先が十分とも限りません。308 を定めた当時の RFC 7538 §4 は、知らない 3xx は 300 と同じに扱われるので、サーバは自動リダイレクトを当てにできないと書いていました。規則が保証するのは、壊れずに一段粗く読まれるところまでです。
意味の無い値を、わざと送る
RFC 8701 の対策は、使っていない値を予約し、クライアントがわざと送ることです。
To avoid this problem, this document reserves some currently unused values for TLS implementations to advertise at random. Correctly implemented peers will ignore these values and interoperate. Peers that do not tolerate unknown values will fail to interoperate, revealing the mistake before it is widespread.
Docker の headless Chromium 151 を、手元の OpenSSL 3.6.3 の s_server -trace に3回接続させました。
ClientHello, Length=1791cipher_suites (len=32){0xCA, 0xCA} UNKNOWN{0x13, 0x01} TLS_AES_128_GCM_SHA256extensions, length = 1686extension_type=UNKNOWN(10794), length=0extension_type=supported_versions(43), length=7UNKNOWN (31354)TLS 1.3 (772)extension_type=key_share(51), length=1263NamedGroup: UNKNOWN (39578)key_exchange: (len=1): 00ServerHello, Length=1206cipher_suite {0x13, 0x01} TLS_AES_128_GCM_SHA256
10794 は 0x2A2A、31354 は 0x7A7A、39578 は 0x9A9A で、どれも RFC 8701 §2 の GREASE 値です。サーバは UNKNOWN と表示して無視し、実在の暗号スイート(暗号の組み合わせを表す2バイトの番号)を選んで握手を完了しました。key_share は鍵交換で使う値を載せる拡張で、NamedGroup はその方式の名前です。どちらも第5回『TLS 1.3 は、相手の答えを推測して1往復を削る』で扱います。ここでは、GREASE の方式名(39578)に付いた key_exchange が長さ1の 00 で、意味を読み込む値ではないことだけ押さえてください。
暗号スイート 拡張 版 鍵交換の方式run 1 {0xDA,0xDA} 0xFAFA 0xDADA 0x7A7A 0xCACArun 2 {0xCA,0xCA} 0x2A2A 0x3A3A 0x7A7A 0x9A9Arun 3 {0x0A,0x0A} 0xFAFA 0x3A3A 0x4A4A 0x2A2A
値は接続ごとに変わります(§1 の advertise at random と整合)。UNKNOWN と表示された値のうち 17613(0x44CD)と 65037(0xFE0D)は GREASE 値ではない別の拡張なので、表から除きました。
一方、OpenSSL 3.6.3 の s_client の ClientHello には、2回とも GREASE 値がありませんでした。送るかどうかはクライアントの任意です(§3.1 A client MAY select one or more GREASE cipher suite values)。「TLS はランダムな値を混ぜることになっている」わけではありません。
HTTP/3 にも同じ発想の予約があります。RFC 9114 は 0x1f * N + 0x21(0x21, 0x40, 0x5f …)の形の値をフレーム型・ストリーム型・SETTINGS などに予約し、SETTINGS には少なくとも1つ入れるべき(SHOULD)としています。
RFC 8701 §3.2 は、GREASE についてサーバに何を求めていますか。
471 を受け取る HTTP クライアント
RFC 9110 §15 は、知らないコードを先頭1桁のクラスの x00 として読むよう求めます。471 なら 400 です。curl の --fail も 22 を返しましたが、それは 400 以上という数値判定で、規則の実装の証拠ではありません。
status=471 exit=22status=400 exit=22status=200 exit=0
GREASE 値を受け取る TLS サーバ
RFC 9846 §4.2.2 は、サーバは知らない拡張を無視すると定めます。Chromium 151 が送った GREASE 値を、OpenSSL 3.6.3 の s_server は UNKNOWN と表示して無視し、3回とも握手を完了しました。
{0xCA, 0xCA} UNKNOWNServerHello, Length=1206cipher_suite {0x13, 0x01} TLS_AES_128_GCM_SHA256
片方はステータスコード、片方は TLS の拡張です。両端が共有しているものは何で、それぞれの例はその共有にどう関わっているでしょうか。
「知らない値」が届く前に、両端がすでに合意していることを探してください。
この章のまとめ
- RFC 9110 §15: クライアントはクラスを理解し、知らないコードをそのクラスの x00 と同等に扱う。471 は例のための未登録の番号。
- 下2桁は分類に使わないだけで、個々のコードの意味はある。
- HTTP のヘッダ・HTTP/2 のフレーム・TLS の拡張にも、知らない値を無視する規則がある。知っている値の規定違反は別で中断の対象。
- 規則は、知らない値が届くまで破られていても気づかれない(RFC 8701 の
rusted shut)。 - GREASE はクライアントが任意で送る意味の無い値。Chromium 151 では接続ごとに値が変わり、サーバは無視して完了した。OpenSSL 3.6.3 の s_client は送らなかった。
curl --failの 22 は 400 以上の数値判定。RFC と結果が一致しただけ。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
RFC 9110 §15 の `The last two digits do not have any categorization role` の読み方として正しいのはどれですか。
用語集(4語)
- 暗号スイート
- TLS で使う暗号の組み合わせを表す2バイトの番号。例: TLS_AES_128_GCM_SHA256。RFC 9846 §4.2.2 は、レコード保護のアルゴリズム(鍵長を含む)と HKDF に使うハッシュの組だと説明する。クライアントが候補を並べ、サーバが1つを選ぶ。
- ステータスコードのクラス
- 先頭1桁で決まる5つの分類(1xx〜5xx)。RFC 9110 §15 は、クライアントはクラスを理解し、知らないコードをそのクラスの x00 と同等に扱わなければならないと定める。
- GREASE
- Generate Random Extensions And Sustain Extensibility。RFC 8701(Informational)が予約した、意味を持たない値。主にクライアントが任意で送り(サーバも CertificateRequest などで送れる。§4)、相手が未知の値を無視できるかを確かめる。
- 錆びついた継ぎ目(rusted shut)
- RFC 8701 §1 の比喩。未知の値を拒む実装が気づかれずに広がり、新しい値を足すと相互接続が壊れる状態。
出典(一次ソース)
- RFC 9110 HTTP Semantics — §15 Status Codes / §5.1 Field Names ↗
- RFC 9113 HTTP/2 — §4.1 Frame Format ↗
- RFC 9114 HTTP/3 — §6.2.3 / §7.2.4.1 / §7.2.8 / §9 ↗
- RFC 9846 The Transport Layer Security (TLS) Protocol Version 1.3(2026年7月、RFC 8446 を廃止)— §4.2.2 / §4.3 ↗
- RFC 8701 Applying GREASE to TLS Extensibility(Informational)— §1 / §2 / §3.1 / §3.2 ↗
- RFC 7538 The HTTP Status Code 308 — §4(RFC 9110 で廃止) ↗
- curl(1) manual — -f, --fail / EXIT CODES 22(確認したのは macOS 同梱版。ページの版表記は curl 8.6.0) ↗