From 25561d975a7a9e10eec3b381bc769737b57b3c9b Mon Sep 17 00:00:00 2001 From: Shadow_Lu Date: Mon, 27 Jul 2026 02:20:53 +0530 Subject: [PATCH] Strip trailing OWS from header values in the C parser (#13246) --- CHANGES/13246.bugfix.rst | 3 +++ CONTRIBUTORS.txt | 1 + aiohttp/_http_parser.pyx | 10 +++++++--- tests/test_http_parser.py | 9 +++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 CHANGES/13246.bugfix.rst diff --git a/CHANGES/13246.bugfix.rst b/CHANGES/13246.bugfix.rst new file mode 100644 index 00000000000..8389dad2ad2 --- /dev/null +++ b/CHANGES/13246.bugfix.rst @@ -0,0 +1,3 @@ +Stripped the trailing whitespace from header values in the C HTTP parser, +so that it matches the pure-Python parser and :rfc:`9110#section-5.5` +-- by :user:`LuShadowX`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 624b9400fbd..d273f6b88d2 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -253,6 +253,7 @@ Lubomir Gelo Ludovic Gasc Luis Pedrosa Lukasz Marcin Dobrzanski +Lukshya Supyal Lénárd Szolnoki Makc Belousow Manuel Miranda diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 73e73cc6773..3808417b0cf 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -430,16 +430,20 @@ cdef class HttpParser: cdef _process_header(self): cdef str value + cdef bytes raw_value if self._raw_name != b"": name = find_header(self._raw_name) - value = self._raw_value.decode('utf-8', 'surrogateescape') + # llhttp strips the OWS before the field value but not after. + # https://www.rfc-editor.org/info/rfc9110/#section-5.5-3 + raw_value = self._raw_value.rstrip(b" \t") + value = raw_value.decode('utf-8', 'surrogateescape') # reject null bytes in header values - matches the Python parser # check at http_parser.py. llhttp in lenient mode doesn't reject # these itself, so we need to catch them here. # ref: RFC 9110 section 5.5 (CTL chars forbidden in field values) if "\x00" in value: - raise InvalidHeader(self._raw_value) + raise InvalidHeader(raw_value) if not self._lax and name in SINGLETON_HEADERS: if name in self._seen_singletons: @@ -454,7 +458,7 @@ cdef class HttpParser: self._has_value = False self._header_name_size = 0 - self._raw_headers.append((self._raw_name, self._raw_value)) + self._raw_headers.append((self._raw_name, raw_value)) self._raw_name = b"" self._raw_value = b"" diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 71e9c783ea0..f080cd208ff 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -645,6 +645,15 @@ def test_whitespace_before_header(parser: HttpRequestParser) -> None: parser.feed_data(text) +def test_whitespace_around_header_value(parser: HttpRequestParser) -> None: + text = b"GET / HTTP/1.1\r\nHost: a\r\ntest: \t value \t \r\n\r\n" + messages, upgrade, tail = parser.feed_data(text) + msg = messages[0][0] + + assert msg.headers["test"] == "value" + assert msg.raw_headers == ((b"Host", b"a"), (b"test", b"value")) + + @pytest.fixture def xfail_c_parser_status(request: pytest.FixtureRequest) -> None: if isinstance(request.getfixturevalue("parser"), HttpRequestParserPy):