From d54e9873e92e64017524ae38cf787925ecfc68aa Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Mon, 19 Oct 2026 19:19:12 +0530 Subject: [PATCH 1/3] fix: handle non-UTF-8 websocket header values --- news/+7600-header-invalid-utf8.bugfix.md | 1 + reflex/app.py | 12 ++++++++- tests/units/test_app.py | 33 +++++++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 news/+7600-header-invalid-utf8.bugfix.md diff --git a/news/+7600-header-invalid-utf8.bugfix.md b/news/+7600-header-invalid-utf8.bugfix.md new file mode 100644 index 00000000000..cd4d6998c00 --- /dev/null +++ b/news/+7600-header-invalid-utf8.bugfix.md @@ -0,0 +1 @@ +Websocket connections now accept request header values containing bytes that are not valid UTF-8. diff --git a/reflex/app.py b/reflex/app.py index 9bcb9892f59..7ac3e356427 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -2084,13 +2084,23 @@ def _sio_loads(data: str | bytes, **kwargs: Any) -> Any: def _decode_asgi_headers(headers: Iterable[tuple[bytes, bytes]]) -> dict[str, str]: """Decode raw ASGI scope header pairs into a str-keyed dict. + Header values that are not valid UTF-8 are decoded as Latin-1. + Args: headers: Raw (name, value) byte pairs from the ASGI scope. Returns: A dict mapping decoded header names to decoded values. """ - return {k.decode("utf-8"): v.decode("utf-8") for (k, v) in headers} + decoded: dict[str, str] = {} + for raw_name, raw_value in headers: + name = raw_name.decode("utf-8") + try: + value = raw_value.decode("utf-8") + except UnicodeDecodeError: + value = raw_value.decode("latin-1") + decoded[name] = value + return decoded class EventNamespace(AsyncNamespace): diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 6227420b31c..f1440eafeed 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -4456,8 +4456,9 @@ def test_decode_asgi_headers(): (b"cookie", b"a=1"), (b"cookie", b"b=2"), ]) == {"cookie": "b=2"} - # Names and values are decoded as UTF-8. + # Valid UTF-8 values are preserved, with Latin-1 fallback for other bytes. assert _decode_asgi_headers([(b"x-name", "café".encode())]) == {"x-name": "café"} + assert _decode_asgi_headers([(b"x-name", b"caf\xe9")]) == {"x-name": "café"} def test_call_ignores_stale_marker_without_dev_backend_reload( @@ -5100,6 +5101,36 @@ async def test_on_connect_processes_boot_event_from_auth( assert state.router_data[constants.RouteVar.CLIENT_TOKEN] == "abc" +@pytest.mark.asyncio +async def test_on_connect_accepts_non_utf8_header_value( + event_namespace: EventNamespace, +): + """A non-UTF-8 request header value does not abort websocket connect. + + Args: + event_namespace: The event namespace. + """ + event_namespace._token_manager = Mock() + event_namespace._token_manager.link_token_to_sid = AsyncMock(return_value=None) + event_namespace.on_event = AsyncMock() + + await event_namespace.on_connect( + "new_sid", + { + "QUERY_STRING": "token=abc", + "asgi.scope": {"headers": [(b"x-name", b"caf\xe9")]}, + }, + {"event": {"name": "state.hydrate_and_load", "payload": {}}}, + ) + + assert ( + event_namespace._static_router_data["new_sid"][constants.RouteVar.HEADERS][ + "x-name" + ] + == "café" + ) + + def _schema_checking_namespace( event_namespace: EventNamespace, backend_schema: str ) -> tuple[AsyncMock, AsyncMock]: From 689684d1a1c7667a315f17209e41ef9c04aed175 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Mon, 19 Oct 2026 19:35:12 +0530 Subject: [PATCH 2/3] test: avoid codespell false positive in header test --- tests/units/test_app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/units/test_app.py b/tests/units/test_app.py index f1440eafeed..698d63bfbe7 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -4457,8 +4457,8 @@ def test_decode_asgi_headers(): (b"cookie", b"b=2"), ]) == {"cookie": "b=2"} # Valid UTF-8 values are preserved, with Latin-1 fallback for other bytes. - assert _decode_asgi_headers([(b"x-name", "café".encode())]) == {"x-name": "café"} - assert _decode_asgi_headers([(b"x-name", b"caf\xe9")]) == {"x-name": "café"} + assert _decode_asgi_headers([(b"x-name", "é".encode())]) == {"x-name": "é"} + assert _decode_asgi_headers([(b"x-name", b"\xe9")]) == {"x-name": "é"} def test_call_ignores_stale_marker_without_dev_backend_reload( @@ -5118,7 +5118,7 @@ async def test_on_connect_accepts_non_utf8_header_value( "new_sid", { "QUERY_STRING": "token=abc", - "asgi.scope": {"headers": [(b"x-name", b"caf\xe9")]}, + "asgi.scope": {"headers": [(b"x-name", b"\xe9")]}, }, {"event": {"name": "state.hydrate_and_load", "payload": {}}}, ) @@ -5127,7 +5127,7 @@ async def test_on_connect_accepts_non_utf8_header_value( event_namespace._static_router_data["new_sid"][constants.RouteVar.HEADERS][ "x-name" ] - == "café" + == "é" ) From 3d43f3c9c5ec56fda33c70cb12956878555082b0 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Wed, 21 Oct 2026 15:48:20 +0530 Subject: [PATCH 3/3] perf: avoid explicit UTF-8 codec lookup for headers --- reflex/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 968b9b41b50..7be5d004647 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -2114,9 +2114,9 @@ def _decode_asgi_headers(headers: Iterable[tuple[bytes, bytes]]) -> dict[str, st """ decoded: dict[str, str] = {} for raw_name, raw_value in headers: - name = raw_name.decode("utf-8") + name = raw_name.decode() try: - value = raw_value.decode("utf-8") + value = raw_value.decode() except UnicodeDecodeError: value = raw_value.decode("latin-1") if name in decoded: