👋

HTTPメッセージの区切り文字はCRLFと定義されているが、実装によってはLFでも通る

に公開

事象

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ではコマンドから復帰せずに待ちの状態が続いてしまった。これがきっかけで調査をした。

GitHubで編集を提案

Discussion