From d953ad57766c5e5625bac52f6d634a15f3ca8975 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 13 Sep 2026 20:09:01 +0200 Subject: [PATCH 1/5] Fix formatting issue. --- src/websockets/sync/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/websockets/sync/server.py b/src/websockets/sync/server.py index d9ffac451..8cb797512 100644 --- a/src/websockets/sync/server.py +++ b/src/websockets/sync/server.py @@ -544,8 +544,8 @@ def handler(websocket): with serve(handler, ...) as server: server.serve_forever() - To stop the server gracefully, call its :meth:`~Server.shutdown` method - from another thread. + To stop the server gracefully, call its :meth:`~Server.shutdown` method from + another thread. Args: handler: Connection handler. It receives the WebSocket connection, From 2a7f710fccc9442298b5643f562211ad5adf5c3a Mon Sep 17 00:00:00 2001 From: Thorvald Natvig Date: Thu, 10 Sep 2026 20:10:08 +0000 Subject: [PATCH 2/5] Fix non-ASCII headers in the legacy HTTP parser. --- src/websockets/legacy/http.py | 2 +- tests/legacy/test_http.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/websockets/legacy/http.py b/src/websockets/legacy/http.py index a7c8a927e..6e7834bc1 100644 --- a/src/websockets/legacy/http.py +++ b/src/websockets/legacy/http.py @@ -175,7 +175,7 @@ async def read_headers(stream: asyncio.StreamReader) -> Headers: name = raw_name.decode("ascii") # guaranteed to be ASCII at this point value = raw_value.decode("ascii", "surrogateescape") - headers[name] = value + headers.set_insecure(name, value) else: raise SecurityError("too many HTTP headers") diff --git a/tests/legacy/test_http.py b/tests/legacy/test_http.py index 76af61122..ade5ad95d 100644 --- a/tests/legacy/test_http.py +++ b/tests/legacy/test_http.py @@ -162,6 +162,11 @@ async def test_header_value(self): with self.assertRaises(ValueError): await read_headers(self.stream) + async def test_header_value_non_ascii(self): + self.stream.feed_data(b"foo: \xc3\xa9\r\n\r\n") + headers = await read_headers(self.stream) + self.assertEqual(headers["foo"], "\udcc3\udca9") + async def test_headers_limit(self): self.stream.feed_data(b"foo: bar\r\n" * 129 + b"\r\n") with self.assertRaises(SecurityError): From f7a3dbffdf4fe360cc7d1f133df97f42daa86ee9 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 12 Sep 2026 13:11:04 +0200 Subject: [PATCH 3/5] Explain why set_insecure is actually safe. --- src/websockets/legacy/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/websockets/legacy/http.py b/src/websockets/legacy/http.py index 6e7834bc1..bb7677ac7 100644 --- a/src/websockets/legacy/http.py +++ b/src/websockets/legacy/http.py @@ -175,6 +175,8 @@ async def read_headers(stream: asyncio.StreamReader) -> Headers: name = raw_name.decode("ascii") # guaranteed to be ASCII at this point value = raw_value.decode("ascii", "surrogateescape") + + # Since we just validated raw_value, we don't need to revalidate it. headers.set_insecure(name, value) else: From 1d5fc1d121583c22b76db74d131d19a9ba5c1fee Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 12 Sep 2026 13:23:50 +0200 Subject: [PATCH 4/5] Align tests with the modern implementation. --- tests/legacy/test_http.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/legacy/test_http.py b/tests/legacy/test_http.py index ade5ad95d..b889ab0d6 100644 --- a/tests/legacy/test_http.py +++ b/tests/legacy/test_http.py @@ -152,21 +152,22 @@ async def test_read_response_invalid_header(self): "invalid HTTP header line: Oops", ) - async def test_header_name(self): + async def test_iso_8859_1_header_value(self): + self.stream.feed_data(b"X-Drink: caf\xe9\r\n\r\n") + headers = await read_headers(self.stream) + # Non-ASCII characters are represented with surrogate escapes. + self.assertEqual(headers["X-Drink"], "caf\udce9") + + async def test_invalid_header_name(self): self.stream.feed_data(b"foo bar: baz qux\r\n\r\n") with self.assertRaises(ValueError): await read_headers(self.stream) - async def test_header_value(self): + async def test_invalid_header_value(self): self.stream.feed_data(b"foo: \x00\x00\x0f\r\n\r\n") with self.assertRaises(ValueError): await read_headers(self.stream) - async def test_header_value_non_ascii(self): - self.stream.feed_data(b"foo: \xc3\xa9\r\n\r\n") - headers = await read_headers(self.stream) - self.assertEqual(headers["foo"], "\udcc3\udca9") - async def test_headers_limit(self): self.stream.feed_data(b"foo: bar\r\n" * 129 + b"\r\n") with self.assertRaises(SecurityError): From ef56ca556848a9c2c72d52f49b7bdbd968f8c0f8 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 13 Sep 2026 20:55:23 +0200 Subject: [PATCH 5/5] Add changelog. --- docs/project/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/project/changelog.rst b/docs/project/changelog.rst index 738288e43..3e147f850 100644 --- a/docs/project/changelog.rst +++ b/docs/project/changelog.rst @@ -44,6 +44,12 @@ Improvements * :func:`~asyncio.client.connect` now closes connections with close code 1011 (internal error) when exiting the context manager with an exception. +Bug fixes +......... + +* Fixed a regression from 16.1 where the legacy implementation rejected + non-ASCII headers. + .. _17.1: 17.1