事象
example.comとpythonで立てたローカルサーバに対して、それぞれhttpリクエストを送ったところ下のような違いが見られた。
# example.com
# 正常パターン
$ printf "GET / HTTP/1.1\r\nHost: example.com\r\nAccept: text/html\r\nConnection: close\r\n\r\n" | nc example.com 80
# 異常パターン
# 結果が返ってこず、コマンドが終わらない (\nを\r\nに一部変更したとしても同じ結果)
$ printf "GET / HTTP/1.1\nHost: example.com\nAccept: text/html\nConnection: close\n\n" | nc example.com 80
# pythonで立てた簡易サーバ
$ python3 -m http.server 8000
# 正常パターン
$ printf "GET / HTTP/1.1\r\nHost: example.com\r\nAccept: text/html\r\nConnection: close\r\n\r\n" | nc localhost 8000
# 正常パターン (こちらも正常に結果が返ってくる)
$ printf "GET / HTTP/1.1\nHost: example.com\nAccept: text/html\nConnection: close\n\n" | nc localhost 8000
この違いが気になったので調査した。
RFCを確認
RFC9112
https://datatracker.ietf.org/doc/html/rfc9112#name-message-format
ここにリクエスト、レスポンスのフォーマットが定義されている
HTTP-message = start-line CRLF
*( field-line CRLF )
CRLF
[ message-body ]
[¶](https://datatracker.ietf.org/doc/html/rfc9112#section-2.1-2)
行の区切り文字はCRLFと明記されている。
RFC2606
https://datatracker.ietf.org/doc/html/rfc2606#section-3
example.comは予約ドメイン。テスト用途として使われる。
example.comはRFCで定められているドメインなので、ルールに厳格なのかもしれない。
つまりRFC9112で定められているようにLF区切りのリクエストは受け付けないのかもしれない。
なお、example.comで立てられているhttpサーバの実装は確認できなかった。
私が見つけることができなかっただけなのか単に非公開になっているだけかは不明。
Pythonのhttpサーバを確認
Pythonのhttpサーバの実装はGitHubにて確認できた。
下記はリクエストメッセージをパースしている部分。ここでsplit()
を使用している。
https://github.com/python/cpython/blob/v3.13.3/Lib/http/server.py#L267
def parse_request(self):
"""Parse a request (internal).
The request should be stored in self.raw_requestline; the results
are in self.command, self.path, self.request_version and
self.headers.
Return True for success, False for failure; on failure, any relevant
error response has already been sent back.
"""
self.command = None # set in case of error on the first line
self.request_version = version = self.default_request_version
self.close_connection = True
requestline = str(self.raw_requestline, 'iso-8859-1')
requestline = requestline.rstrip('\r\n')
self.requestline = requestline
words = requestline.split()
# 省略
コマンドで実験してみるとsplit()
はCRLFでもLFでも分割することを確認できた。
>>> "abc\r\ndef\r\nghi".split()
['abc', 'def', 'ghi']
>>> "abc\ndef\nghi".split()
['abc', 'def', 'ghi']
>>> "abc\ndef\r\nghi".split()
['abc', 'def', 'ghi']
>>>
さらに調べてみるとsplit()
の実装はここで見られることがわかった。
https://github.com/python/cpython/blob/v3.13.3/Objects/stringlib/split.h#L363
実験通りCRLF, LFどちらにも対応している。
結論
HTTPメッセージの区切り文字はRFC9112にてCRLFと定められている。
実際は各httpサーバの実装によって、LFでも処理されるように柔軟さを持っていることがある。
調査のきっかけ
「作って学ぶブラウザのしくみ」という書籍がある。
https://direct.gihyo.jp/view/item/000000003560
この書籍では実際にhttpサーバを実装していくのだが、リクエストメッセージを\n
区切りとして実装している。
https://github.com/d0iasm/sababook/blob/main/ch3/saba/net/wasabi/src/http.rs#L45
let mut request = String::from("GET /");
request.push_str(&path);
request.push_str(" HTTP/1.1\n");
// ヘッダの追加
request.push_str("Host: ");
request.push_str(&host);
request.push('\n');
request.push_str("Accept: text/html\n");
request.push_str("Connection: close\n");
request.push('\n');
そして本書p90-p95では、実装したhttpサーバを用いてリクエストを送る実験を載せている。
リクエスト先は、冒頭に書いたexample.com
とpythonで立てたローカルサーバ。本書にしたがって実験してみたところ、実際にはexample.com
ではコマンドから復帰せずに待ちの状態が続いてしまった。これがきっかけで調査をした。