From c29a7b3751ad93043bf99fd74f22e0fe81192707 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 18:21:34 +0000 Subject: [PATCH 1/6] Remove stale rich-markup escapes from logging pipeline messages The 0.9.9 migration from the legacy console helpers to stdlib logging kept the rich-markup escaping (escape() / .replace("[", "\\[")) that the old markup-rendering path consumed. The new RichConsoleHandler renders records with markup=False unless a record opts in via extra={"rich": True}, so the leftover escapes now print literally: compile-time warnings such as the event-handler arg-mismatch warning showed "dict\[str, typing.Any]" instead of "dict[str, typing.Any]" (FINDING-027 from 0.9.9a1 pre-release testing, a cosmetic regression vs 0.9.8). Drop the escaping at every call site that logs through the pipeline without the markup opt-in: the event arg-mismatch warning, the None-to-non-Optional Var warning, the computed-var return-type errors, the event-queue processing error, and the state field type error. Bracketed text now renders verbatim, and markup injection from user-controlled values stays impossible because the sink keeps markup disabled. Call sites that opt into markup (extra={"rich": True}) keep their escapes, as does the escaping of client-supplied data placed into Exception objects for user-defined handlers. Regression tests capture the rendered console output: the arg-mismatch warning must contain clean "dict[str, str]" with no backslashes, and a warning containing markup-like text ("[red]x[/red]") must render it literally without styling. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x --- news/+warning-escape-leak.bugfix.md | 1 + .../src/reflex_base/components/component.py | 3 +- .../src/reflex_base/event/__init__.py | 6 ++-- .../event/processor/event_processor.py | 5 +-- .../reflex-base/src/reflex_base/vars/base.py | 2 +- reflex/state.py | 4 +-- tests/units/reflex_base/utils/test_log.py | 12 +++++++ tests/units/test_event.py | 36 ++++++++++++++++++- 8 files changed, 55 insertions(+), 14 deletions(-) create mode 100644 news/+warning-escape-leak.bugfix.md diff --git a/news/+warning-escape-leak.bugfix.md b/news/+warning-escape-leak.bugfix.md new file mode 100644 index 00000000000..057e278d419 --- /dev/null +++ b/news/+warning-escape-leak.bugfix.md @@ -0,0 +1 @@ +Console warnings and errors no longer print literal backslash-escaped brackets (e.g. `dict\[str, str]`). The rich-markup escapes were left over from the legacy console helpers, but the logging pipeline renders messages with markup disabled, so bracketed type names now print verbatim. diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index 80345c22e59..6bb6888ccc5 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -19,7 +19,6 @@ from types import SimpleNamespace from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast -from rich.markup import escape from typing_extensions import dataclass_transform from reflex_base import constants @@ -586,7 +585,7 @@ def satisfies_type_hint(obj: Any, type_hint: Any) -> bool: ) logger.warning( "Passing None to a Var that is not explicitly marked as Optional (| None) is deprecated. " - f"Passed {obj!s} of type {escape(str(type(obj) if not isinstance(obj, Var) else obj._var_type))} to {escape(str(type_hint))}." + f"Passed {obj!s} of type {type(obj) if not isinstance(obj, Var) else obj._var_type} to {type_hint}." ) return True return False diff --git a/packages/reflex-base/src/reflex_base/event/__init__.py b/packages/reflex-base/src/reflex_base/event/__init__.py index 6f0f0d9238b..b191bd93349 100644 --- a/packages/reflex-base/src/reflex_base/event/__init__.py +++ b/packages/reflex-base/src/reflex_base/event/__init__.py @@ -2021,14 +2021,12 @@ def _check_event_args_subclass_of_callback( for arg in args ] - expect_string = ", ".join( - repr(arg) for arg in args_types_without_vars - ).replace("[", "\\[") + expect_string = ", ".join(repr(arg) for arg in args_types_without_vars) given_string = ", ".join( repr(callback_param_name_to_type.get(arg, Any)) for arg in callback_params_names - ).replace("[", "\\[") + ) as_annotated_in = ( f" as annotated in {callback_name}" if callback_name else "" diff --git a/packages/reflex-base/src/reflex_base/event/processor/event_processor.py b/packages/reflex-base/src/reflex_base/event/processor/event_processor.py index fcdc90eade5..4f106c44bb9 100644 --- a/packages/reflex-base/src/reflex_base/event/processor/event_processor.py +++ b/packages/reflex-base/src/reflex_base/event/processor/event_processor.py @@ -14,7 +14,6 @@ from contextvars import Token, copy_context from typing import TYPE_CHECKING, Any, TypeVar -import rich.markup from typing_extensions import Self from reflex.app_mixins.middleware import MiddlewareMixin @@ -749,9 +748,7 @@ async def _process_queue(self): except Exception: # Log the error and continue processing the next events. logger.exception( - rich.markup.escape( - f"Error processing event queue entry for {entry.event} [txid={entry.ctx.txid}]:" - ) + f"Error processing event queue entry for {entry.event} [txid={entry.ctx.txid}]:" ) queue.task_done() if self._queue_task is asyncio.current_task(): diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index 676bf16a8d6..69d40fb7ff0 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -2613,7 +2613,7 @@ def _check_deprecated_return_type(self, instance: BaseState, value: Any) -> None if not _isinstance(value, self._var_type, nested=1, treat_var_as_type=False): logger.error( f"Computed var '{type(instance).__name__}.{self._name}' must return" - f" a value of type '{escape(str(self._var_type))}', got '{value!s}' of type {type(value)}." + f" a value of type '{self._var_type}', got '{value!s}' of type {type(value)}." ) def _deps( diff --git a/reflex/state.py b/reflex/state.py index 452c80e7109..15722061c65 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -813,7 +813,7 @@ def computed_var_func(state: Self): if not _isinstance(result, of_type, nested=1, treat_var_as_type=False): logger.warning( - f"Inline ComputedVar {f} expected type {escape(str(of_type))}, got {type(result)}. " + f"Inline ComputedVar {f} expected type {of_type}, got {type(result)}. " "You can specify expected type with `of_type` argument." ) @@ -1543,7 +1543,7 @@ def __setattr__(self, name: str, value: Any): field_type = field.outer_type_ if not _isinstance(value, field_type, nested=1, treat_var_as_type=False): logger.error( - f"Expected field '{type(self).__name__}.{name}' to receive type '{escape(str(field_type))}'," + f"Expected field '{type(self).__name__}.{name}' to receive type '{field_type}'," f" but got '{value}' of type '{type(value)}'." ) diff --git a/tests/units/reflex_base/utils/test_log.py b/tests/units/reflex_base/utils/test_log.py index f204943012b..698a58c3368 100644 --- a/tests/units/reflex_base/utils/test_log.py +++ b/tests/units/reflex_base/utils/test_log.py @@ -71,6 +71,18 @@ def test_markup_literal_by_default(capsys): assert out == "Info: AssertionErr: foo[bar] != 'baz'\n" +def test_warning_with_markup_tags_stays_literal(capsys): + """User data resembling rich markup is neither styled nor escaped. + + Messages must not be markup-escaped at the call site: the sink renders + them raw, so the escape backslashes would print literally, while markup + injection stays impossible because markup is off by default. + """ + logger.warning("got [red]x[/red] of type dict[str, str]") + out, _ = capsys.readouterr() + assert out == "Warning: got [red]x[/red] of type dict[str, str]\n" + + def test_level_gating(capsys): """Records below the configured level are dropped.""" log.set_log_level(LogLevel.WARNING) diff --git a/tests/units/test_event.py b/tests/units/test_event.py index fa07a89dcc6..7194b746641 100644 --- a/tests/units/test_event.py +++ b/tests/units/test_event.py @@ -3,6 +3,7 @@ from typing import Any, cast import pytest +from reflex_base.constants import LogLevel from reflex_base.constants.compiler import Hooks, Imports from reflex_base.event import ( BACKGROUND_TASK_MARKER, @@ -15,10 +16,13 @@ call_event_handler, event, fix_events, + on_submit_event, + on_submit_string_event, ) -from reflex_base.utils import format +from reflex_base.utils import format, log from reflex_base.utils.exceptions import EventHandlerValueError from reflex_base.vars.base import Field, LiteralVar, Var, field +from rich.console import Console import reflex as rx from reflex.state import BaseState @@ -1281,3 +1285,33 @@ def test_decentralized_event_global_state(): """Test the decentralized event with a global state.""" _ = rx.input(on_change=f("foo")) _ = rx.input(on_change=f) + + +def test_arg_mismatch_warning_renders_brackets_verbatim(capsys, monkeypatch): + """The arg-mismatch warning renders bracketed type names without escapes. + + The rich console sink prints records with markup disabled, so the message + must not carry rich-markup escapes: the backslashes would print literally. + """ + + def handle_submit(form_data: dict[str, str]): + pass + + handle_submit.__qualname__ = "handle_submit" + + monkeypatch.setenv(log._MANAGED_ENV_VAR, "true") + monkeypatch.setattr(log, "_log_level", LogLevel.INFO) + # A wide console so the long warning is not line-wrapped mid-assertion. + monkeypatch.setattr(log, "_console", Console(highlight=False, width=1000)) + log.configure() + try: + call_event_handler( + EventHandler(fn=handle_submit), + (on_submit_event, on_submit_string_event), + key="on_submit", + ) + out, _ = capsys.readouterr() + finally: + log._reset() + assert "expects (dict[str, typing.Any]) -> () but got (dict[str, str]) -> ()" in out + assert "\\" not in out From 0c97ab9d3483c8279bc66903a91436bde3ac8aa2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 19:20:39 +0000 Subject: [PATCH 2/6] Add reflex-base news fragment for the escape-leak fix The changelog gate requires a fragment in each package whose sources the PR touches; the fix edits reflex-base modules but only carried the root news entry. Mirror it under packages/reflex-base/news/ so reflex-release changelog-check passes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x --- packages/reflex-base/news/+warning-escape-leak.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/reflex-base/news/+warning-escape-leak.bugfix.md diff --git a/packages/reflex-base/news/+warning-escape-leak.bugfix.md b/packages/reflex-base/news/+warning-escape-leak.bugfix.md new file mode 100644 index 00000000000..057e278d419 --- /dev/null +++ b/packages/reflex-base/news/+warning-escape-leak.bugfix.md @@ -0,0 +1 @@ +Console warnings and errors no longer print literal backslash-escaped brackets (e.g. `dict\[str, str]`). The rich-markup escapes were left over from the legacy console helpers, but the logging pipeline renders messages with markup disabled, so bracketed type names now print verbatim. From 0b165994543056addcaaead361bd68289017b714 Mon Sep 17 00:00:00 2001 From: Farhan Date: Sat, 29 Aug 2026 00:22:58 +0500 Subject: [PATCH 3/6] Drop rich-markup escapes from VarAttributeError messages The messages land in plain tracebacks, never in a markup-enabled sink, so the escaped brackets printed literally (dict\[str, int]). Claude-Session: https://claude.ai/code/session_01JV6TsCnMzU4kyZ9ejSPyqY --- news/+warning-escape-leak.bugfix.md | 2 +- packages/reflex-base/src/reflex_base/vars/base.py | 3 +-- packages/reflex-base/src/reflex_base/vars/object.py | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/news/+warning-escape-leak.bugfix.md b/news/+warning-escape-leak.bugfix.md index 057e278d419..6d1b196d52f 100644 --- a/news/+warning-escape-leak.bugfix.md +++ b/news/+warning-escape-leak.bugfix.md @@ -1 +1 @@ -Console warnings and errors no longer print literal backslash-escaped brackets (e.g. `dict\[str, str]`). The rich-markup escapes were left over from the legacy console helpers, but the logging pipeline renders messages with markup disabled, so bracketed type names now print verbatim. +Console warnings and errors no longer print literal backslash-escaped brackets (e.g. `dict\[str, str]`). The rich-markup escapes were left over from the legacy console helpers, but the logging pipeline renders messages with markup disabled, so bracketed type names now print verbatim. `VarAttributeError` messages drop the same escapes. diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index 69d40fb7ff0..8803960d51c 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -38,7 +38,6 @@ overload, ) -from rich.markup import escape from typing_extensions import LiteralString, dataclass_transform, override from reflex_base import constants @@ -1484,7 +1483,7 @@ def __getattr__(self, name: str): f"access the attribute '{name}'", ) - msg = f"The State var {escape(self._js_expr)} of type {escape(str(self._var_type))} has no attribute '{name}' or may have been annotated wrongly." + msg = f"The State var {self._js_expr} of type {self._var_type} has no attribute '{name}' or may have been annotated wrongly." raise VarAttributeError(msg) def __bool__(self) -> bool: diff --git a/packages/reflex-base/src/reflex_base/vars/object.py b/packages/reflex-base/src/reflex_base/vars/object.py index bf10fdbb993..ab809eeb19f 100644 --- a/packages/reflex-base/src/reflex_base/vars/object.py +++ b/packages/reflex-base/src/reflex_base/vars/object.py @@ -17,8 +17,6 @@ overload, ) -from rich.markup import escape - from reflex_base.utils import types from reflex_base.utils.exceptions import VarAttributeError from reflex_base.utils.types import ( @@ -348,7 +346,7 @@ def __getattr__(self, name: str) -> Var: if attribute_type is None: msg = ( - f"The State var `{self!s}` of type {escape(str(self._var_type))} has no attribute '{name}' or may have been annotated " + f"The State var `{self!s}` of type {self._var_type} has no attribute '{name}' or may have been annotated " f"wrongly." ) raise VarAttributeError(msg) From d5e03445afae41b1d7af231d6d2362150c0d2935 Mon Sep 17 00:00:00 2001 From: Farhan Date: Sat, 29 Aug 2026 00:27:51 +0500 Subject: [PATCH 4/6] Mention VarAttributeError in reflex-base news fragment --- packages/reflex-base/news/+warning-escape-leak.bugfix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/reflex-base/news/+warning-escape-leak.bugfix.md b/packages/reflex-base/news/+warning-escape-leak.bugfix.md index 057e278d419..6d1b196d52f 100644 --- a/packages/reflex-base/news/+warning-escape-leak.bugfix.md +++ b/packages/reflex-base/news/+warning-escape-leak.bugfix.md @@ -1 +1 @@ -Console warnings and errors no longer print literal backslash-escaped brackets (e.g. `dict\[str, str]`). The rich-markup escapes were left over from the legacy console helpers, but the logging pipeline renders messages with markup disabled, so bracketed type names now print verbatim. +Console warnings and errors no longer print literal backslash-escaped brackets (e.g. `dict\[str, str]`). The rich-markup escapes were left over from the legacy console helpers, but the logging pipeline renders messages with markup disabled, so bracketed type names now print verbatim. `VarAttributeError` messages drop the same escapes. From 2ec099329f4fcc497cfa13f1b8496600132d8b1f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 19:28:03 +0000 Subject: [PATCH 5/6] Drop leftover rich-markup escapes from VarAttributeError messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer follow-up: the same stale escaping fixed for warnings also lived in the VarAttributeError messages raised by Var.__getattr__ and ObjectVar.__getattr__, which land in plain tracebacks — so bracketed type names printed as dict\[str, int]. Removes the escape() calls and now-unused imports, extends the news fragments, and adds a regression test that fails against the unescaped-message-free sources. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x --- tests/units/test_var.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/units/test_var.py b/tests/units/test_var.py index 942d7d78d31..3db58f025a7 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -2308,3 +2308,21 @@ async def async_wrapper(self) -> str: rx.input(placeholder=ComputedVarTypeState.sync_wrapper) rx.input(placeholder=ComputedVarTypeState.async_plain) rx.input(placeholder=ComputedVarTypeState.async_wrapper) + + +def test_var_attribute_error_renders_brackets_verbatim(): + r"""VarAttributeError messages carry no rich-markup escapes. + + The messages end up in plain tracebacks, so escaped brackets would print + literally as ``dict\[str, int]``. + """ + + class Point(typing.TypedDict): + x: int + + with pytest.raises( + AttributeError, match=r"of type dict\[str, int\] has no attribute" + ): + _ = Var(_js_expr="x", _var_type=dict[str, int]).foo # pyright: ignore[reportAttributeAccessIssue] + with pytest.raises(AttributeError, match=r"of type .*Point.* has no attribute"): + _ = Var(_js_expr="x", _var_type=Point).guess_type().foo # pyright: ignore[reportAttributeAccessIssue] From eeba048485d04d16a6a5f48d85205eb7801f7981 Mon Sep 17 00:00:00 2001 From: Farhan Date: Sat, 29 Aug 2026 00:30:13 +0500 Subject: [PATCH 6/6] Remove test asserting on the dropped markup escapes Testing that a removed escape stays removed is not worth a test. Claude-Session: https://claude.ai/code/session_01JV6TsCnMzU4kyZ9ejSPyqY --- tests/units/test_var.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/units/test_var.py b/tests/units/test_var.py index 3db58f025a7..942d7d78d31 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -2308,21 +2308,3 @@ async def async_wrapper(self) -> str: rx.input(placeholder=ComputedVarTypeState.sync_wrapper) rx.input(placeholder=ComputedVarTypeState.async_plain) rx.input(placeholder=ComputedVarTypeState.async_wrapper) - - -def test_var_attribute_error_renders_brackets_verbatim(): - r"""VarAttributeError messages carry no rich-markup escapes. - - The messages end up in plain tracebacks, so escaped brackets would print - literally as ``dict\[str, int]``. - """ - - class Point(typing.TypedDict): - x: int - - with pytest.raises( - AttributeError, match=r"of type dict\[str, int\] has no attribute" - ): - _ = Var(_js_expr="x", _var_type=dict[str, int]).foo # pyright: ignore[reportAttributeAccessIssue] - with pytest.raises(AttributeError, match=r"of type .*Point.* has no attribute"): - _ = Var(_js_expr="x", _var_type=Point).guess_type().foo # pyright: ignore[reportAttributeAccessIssue]