From b5106165de01fdbc43c913f4222c1a09deb64f2e Mon Sep 17 00:00:00 2001 From: xic Date: Thu, 3 Sep 2026 12:07:22 +0800 Subject: [PATCH 1/4] refactor: migrate httpx to httpx2 with dual import Use the actively maintained httpx2 fork (Pydantic Services) when available, falling back to httpx. All 7 internal call sites are CLI/framework tooling (no AsyncClient, no public API exposure). The private import in net.py (get_environment_proxies) is also dual-bound because httpx2 ships an equivalent helper. Refs: #7034 --- pyproject.toml | 1 + reflex/custom_components/custom_components.py | 5 +- reflex/utils/frontend_skeleton.py | 5 +- reflex/utils/js_runtimes.py | 5 +- reflex/utils/net.py | 23 +++++++-- reflex/utils/registry.py | 5 +- reflex/utils/telemetry.py | 5 +- reflex/utils/templates.py | 5 +- tests/units/test_telemetry.py | 6 ++- tests/units/utils/test_utils.py | 5 +- uv.lock | 47 ++++++++++++------- 11 files changed, 81 insertions(+), 31 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7fa2954d18c..190d90dc275 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "click >=8.2", "granian[reload] >=2.7.4", "httpx >=0.26,<1.0", + "httpx2 >=2.0; python_version >= '3.10'", "packaging >=24.2,<27", "psutil >=7.0.0,<8.0; sys_platform == 'win32'", "python-multipart >=0.0.32,<1.0", diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index 35dd3bcb4da..2e97bb799e6 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -634,7 +634,10 @@ def _collect_details_for_gallery(): Raises: SystemExit: If pyproject.toml file is ill-formed or the request to the backend services fails. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx from reflex_cli.utils import hosting console.rule("[bold]Authentication with Reflex Services") diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index 9a5fa3d9ee3..55590de7897 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -141,7 +141,10 @@ def initialize_agents_md( """ plan = _plan_agents_md(agents_file, claude_file) - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx logger.debug(f"Fetching {url}") try: diff --git a/reflex/utils/js_runtimes.py b/reflex/utils/js_runtimes.py index 2f5b9a5188d..469fdd7ddce 100644 --- a/reflex/utils/js_runtimes.py +++ b/reflex/utils/js_runtimes.py @@ -234,7 +234,10 @@ def download_and_run(url: str, *args, show_status: bool = False, **env): Raises: SystemExit: If the script fails to download. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx # Download the script logger.debug(f"Downloading {url}") diff --git a/reflex/utils/net.py b/reflex/utils/net.py index ffa96b95ce3..cafc844fdf1 100644 --- a/reflex/utils/net.py +++ b/reflex/utils/net.py @@ -41,7 +41,10 @@ def _wrap_https_func( @functools.wraps(func) def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T: - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx url = args[0] logger.debug(f"Sending HTTPS request to {args[0]}") @@ -95,7 +98,10 @@ def _is_ipv4_supported() -> bool: Returns: True if the system supports IPv4, False otherwise. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx try: httpx.head("http://1.1.1.1", timeout=3) @@ -111,7 +117,10 @@ def _is_ipv6_supported() -> bool: Returns: True if the system supports IPv6, False otherwise. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx try: httpx.head("http://[2606:4700:4700::1111]", timeout=3) @@ -150,8 +159,12 @@ def _httpx_client(): Returns: An HTTPX client. """ - import httpx - from httpx._utils import get_environment_proxies + try: + import httpx2 as httpx + from httpx2._utils import get_environment_proxies + except ModuleNotFoundError: + import httpx + from httpx._utils import get_environment_proxies verify_setting = _httpx_verify_kwarg() return httpx.Client( diff --git a/reflex/utils/registry.py b/reflex/utils/registry.py index 994f7ea1ae0..b34dbc16b9d 100644 --- a/reflex/utils/registry.py +++ b/reflex/utils/registry.py @@ -20,7 +20,10 @@ def latency(registry: str) -> int: Returns: int: The latency of the registry in microseconds. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx try: time_to_respond = net.get(registry, timeout=2).elapsed.microseconds diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index a1d76d2e8b5..334b15cebf9 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -435,7 +435,10 @@ def _prepare_event( def _send_event(event_data: _Event) -> bool: - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx try: httpx.post(POSTHOG_API_URL, json=event_data) diff --git a/reflex/utils/templates.py b/reflex/utils/templates.py index f47976e28ad..e0203ec4b3e 100644 --- a/reflex/utils/templates.py +++ b/reflex/utils/templates.py @@ -122,7 +122,10 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str SystemExit: If any download, file operations fail or unexpected zip file format. """ - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx # Create a temp directory for the zip download. try: diff --git a/tests/units/test_telemetry.py b/tests/units/test_telemetry.py index aa530e39ccf..7ee3c8c0985 100644 --- a/tests/units/test_telemetry.py +++ b/tests/units/test_telemetry.py @@ -66,7 +66,11 @@ def httpx_post(mocker: MockerFixture): Returns: The mock for ``httpx.post`` so tests can assert on the posted payload. """ - return mocker.patch("httpx.post") + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx + return mocker.patch.object(httpx, "post") def test_telemetry(): diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 98ebe80475b..d643124c5a4 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -537,7 +537,10 @@ def test_initialize_agents_md_refreshes_managed_section(tmp_path, mocker): def test_initialize_agents_md_warns_on_fetch_failure(tmp_path, mocker, caplog): """Test that a failed fetch warns without writing AGENTS.md or the bridge.""" - import httpx + try: + import httpx2 as httpx + except ModuleNotFoundError: + import httpx agents_file = tmp_path / "AGENTS.md" claude_file = tmp_path / "CLAUDE.md" diff --git a/uv.lock b/uv.lock index 43f7d18e4d3..034b43b0cf9 100644 --- a/uv.lock +++ b/uv.lock @@ -15,7 +15,7 @@ resolution-markers = [ ] [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-08-27T03:10:58.998873Z" exclude-newer-span = "P7D" [options.exclude-newer-package] @@ -659,7 +659,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -1068,7 +1068,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1993,15 +1993,15 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" } }, - { name = "cycler" }, - { name = "fonttools" }, - { name = "kiwisolver" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "pillow" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "cycler", marker = "python_full_version < '3.11'" }, + { name = "fonttools", marker = "python_full_version < '3.11'" }, + { name = "kiwisolver", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pillow", marker = "python_full_version < '3.11'" }, + { name = "pyparsing", marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } wheels = [ @@ -2659,10 +2659,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -3767,6 +3767,7 @@ dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, { name = "httpx" }, + { name = "httpx2" }, { name = "packaging" }, { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "python-multipart" }, @@ -3858,6 +3859,7 @@ requires-dist = [ { name = "click", specifier = ">=8.2" }, { name = "granian", extras = ["reload"], specifier = ">=2.7.4" }, { name = "httpx", specifier = ">=0.26,<1.0" }, + { name = "httpx2", marker = "python_full_version >= '3.10'", specifier = ">=2.0" }, { name = "packaging", specifier = ">=24.2,<27" }, { name = "psutil", marker = "sys_platform == 'win32'", specifier = ">=7.0.0,<8.0" }, { name = "pydantic", marker = "extra == 'db'", specifier = ">=2.12.0,<3.0" }, @@ -4431,7 +4433,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -4492,7 +4494,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -4997,6 +4999,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, ] +[[package]] +name = "ty" +version = "0.0.69" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "ty" version = "0.0.73" From 471b42b5a67cab28463db55af18c9f54a8e5349e Mon Sep 17 00:00:00 2001 From: xic Date: Fri, 4 Sep 2026 10:26:02 +0800 Subject: [PATCH 2/4] fix(reflex): suppress pyright min-version union errors on httpx2/httpx dual-import Signed-off-by: xic --- reflex/utils/net.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/reflex/utils/net.py b/reflex/utils/net.py index cafc844fdf1..4d83372ba53 100644 --- a/reflex/utils/net.py +++ b/reflex/utils/net.py @@ -159,16 +159,29 @@ def _httpx_client(): Returns: An HTTPX client. """ + # Resolve the active HTTP library at call time. Prefer httpx2 when + # available, fall back to real httpx on Python 3.8/3.9 (which httpx2 + # cannot run on). Bind the classes to local names so pyright does not + # infer a union of `httpx2.HTTPTransport | httpx.HTTPTransport` — + # that union is not assignable to `Client(mounts=...)` because the + # two transport classes are unrelated. try: - import httpx2 as httpx + import httpx2 from httpx2._utils import get_environment_proxies except ModuleNotFoundError: - import httpx - from httpx._utils import get_environment_proxies + import httpx as httpx2 # noqa: F401 — local name `httpx2` bound to the real httpx + from httpx._utils import get_environment_proxies # noqa: F811 verify_setting = _httpx_verify_kwarg() - return httpx.Client( - transport=httpx.HTTPTransport( + # `httpx2` is a union of the two modules here (httpx2 in the try + # branch, real httpx in the except branch). The two HTTPTransport + # / Proxy / Client classes share compatible shapes but pyright in + # min-version mode still infers a union and rejects the assignment + # to `BaseTransport` / `ProxyTypes`. In practice only one branch + # runs per process; the `# type: ignore` below is the smallest way + # to tell pyright that, suppressing the no-real-error warnings. + return httpx2.Client( # type: ignore[call-overload] + transport=httpx2.HTTPTransport( # type: ignore[arg-type] local_address=_httpx_local_address_kwarg(), verify=verify_setting, ), @@ -176,8 +189,9 @@ def _httpx_client(): key: ( None if url is None - else httpx.HTTPTransport( - proxy=httpx.Proxy(url=url), verify=verify_setting + else httpx2.HTTPTransport( # type: ignore[arg-type] + proxy=httpx2.Proxy(url=url), # type: ignore[arg-type] + verify=verify_setting, ) ) for key, url in get_environment_proxies().items() @@ -185,4 +199,4 @@ def _httpx_client(): ) -get = _wrap_https_lazy_func(lambda: _httpx_client().get) +get = _wrap_https_lazy_func(lambda: _httpx_client().get) # type: ignore[arg-type] From e71e2e56e9695fe860f0231302cd511f2c5269e8 Mon Sep 17 00:00:00 2001 From: xic Date: Mon, 7 Sep 2026 18:33:04 +0800 Subject: [PATCH 3/4] fix(reflex): hard switch to httpx2 (no dual-import shim, no alias) Use direct 'import httpx2' across the main package: drop the 'import httpx2 as httpx' aliasing and the two remaining ModuleNotFoundError fallbacks in tests. Remove the httpx runtime dependency; httpx2>=2.12.0 is unconditional (requires-python is already >=3.10). Repair uv.lock: replace a corrupt duplicate 'ty' entry (0.0.69 pointing at truststore artifacts, rejected by uv) and record the httpx2 swap. reflex-hosting-cli (separate distribution, pins httpx <1.0) and reflex-site-shared keep httpx for now; httpx remains installed transitively via reflex-hosting-cli. Refs: #7034 --- pyproject.toml | 3 +- reflex/custom_components/custom_components.py | 15 ++--- reflex/utils/frontend_skeleton.py | 7 +-- reflex/utils/js_runtimes.py | 7 +-- reflex/utils/net.py | 35 ++++------- reflex/utils/registry.py | 7 +-- reflex/utils/telemetry.py | 7 +-- reflex/utils/templates.py | 7 +-- tests/test_node_version.py | 4 +- tests/units/test_telemetry.py | 12 ++-- tests/units/utils/test_utils.py | 7 +-- uv.lock | 59 +++++++++---------- 12 files changed, 65 insertions(+), 105 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 190d90dc275..b12018d16de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,8 +22,7 @@ requires-python = ">=3.10,<4.0" dependencies = [ "click >=8.2", "granian[reload] >=2.7.4", - "httpx >=0.26,<1.0", - "httpx2 >=2.0; python_version >= '3.10'", + "httpx2 >=2.12.0", "packaging >=24.2,<27", "psutil >=7.0.0,<8.0; sys_platform == 'win32'", "python-multipart >=0.0.32,<1.0", diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index 2e97bb799e6..3f5080ca280 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -634,10 +634,7 @@ def _collect_details_for_gallery(): Raises: SystemExit: If pyproject.toml file is ill-formed or the request to the backend services fails. """ - try: - import httpx2 as httpx - except ModuleNotFoundError: - import httpx + import httpx2 from reflex_cli.utils import hosting console.rule("[bold]Authentication with Reflex Services") @@ -669,18 +666,18 @@ def _collect_details_for_gallery(): # Send a POST request to achieve two things at once: # 1. Check if the package is already shared by the user. If not, the backend will return 403. # 2. If this package is not shared before, this request records the package name in the backend. - response = httpx.post( + response = httpx2.post( post_custom_components_gallery_endpoint, headers={"Authorization": f"Bearer {access_token}"}, data=params, ) - if response.status_code == httpx.codes.FORBIDDEN: + if response.status_code == httpx2.codes.FORBIDDEN: logger.error( f"{package_name} is owned by another user. Unable to update the information for it." ) raise SystemExit(1) response.raise_for_status() - except httpx.HTTPError as he: + except httpx2.HTTPError as he: logger.error(f"Unable to complete request due to {he}.") raise SystemExit(1) from None @@ -707,7 +704,7 @@ def _collect_details_for_gallery(): # Now send the post request to Reflex backend services. try: logger.debug(f"Sending custom component data: {params}") - response = httpx.post( + response = httpx2.post( post_custom_components_gallery_endpoint, headers={"Authorization": f"Bearer {access_token}"}, data=params, @@ -716,7 +713,7 @@ def _collect_details_for_gallery(): ) response.raise_for_status() - except httpx.HTTPError as he: + except httpx2.HTTPError as he: logger.error(f"Unable to complete request due to {he}.") raise SystemExit(1) from None diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index 55590de7897..e899cd1a3f8 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -141,16 +141,13 @@ def initialize_agents_md( """ plan = _plan_agents_md(agents_file, claude_file) - try: - import httpx2 as httpx - except ModuleNotFoundError: - import httpx + import httpx2 logger.debug(f"Fetching {url}") try: response = net.get(url, timeout=5) response.raise_for_status() - except httpx.HTTPError as e: + except httpx2.HTTPError as e: logger.warning(f"Failed to fetch AGENTS.md from {url} due to {e}. Skipping.") return diff --git a/reflex/utils/js_runtimes.py b/reflex/utils/js_runtimes.py index 469fdd7ddce..f1a2127fe90 100644 --- a/reflex/utils/js_runtimes.py +++ b/reflex/utils/js_runtimes.py @@ -234,17 +234,14 @@ def download_and_run(url: str, *args, show_status: bool = False, **env): Raises: SystemExit: If the script fails to download. """ - try: - import httpx2 as httpx - except ModuleNotFoundError: - import httpx + import httpx2 # Download the script logger.debug(f"Downloading {url}") try: response = net.get(url) response.raise_for_status() - except httpx.HTTPError as e: + except httpx2.HTTPError as e: logger.error( f"Failed to download bun install script. You can install or update bun manually from https://bun.com \n{e}" ) diff --git a/reflex/utils/net.py b/reflex/utils/net.py index 4d83372ba53..7938087b2db 100644 --- a/reflex/utils/net.py +++ b/reflex/utils/net.py @@ -41,17 +41,14 @@ def _wrap_https_func( @functools.wraps(func) def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T: - try: - import httpx2 as httpx - except ModuleNotFoundError: - import httpx + import httpx2 url = args[0] logger.debug(f"Sending HTTPS request to {args[0]}") initial_time = time.time() try: response = func(*args, **kwargs) - except httpx.ConnectError as err: + except httpx2.ConnectError as err: if "CERTIFICATE_VERIFY_FAILED" in str(err): # If the error is a certificate verification error, recommend mitigating steps. logger.error( @@ -98,14 +95,11 @@ def _is_ipv4_supported() -> bool: Returns: True if the system supports IPv4, False otherwise. """ - try: - import httpx2 as httpx - except ModuleNotFoundError: - import httpx + import httpx2 try: - httpx.head("http://1.1.1.1", timeout=3) - except httpx.RequestError: + httpx2.head("http://1.1.1.1", timeout=3) + except httpx2.RequestError: return False else: return True @@ -117,14 +111,11 @@ def _is_ipv6_supported() -> bool: Returns: True if the system supports IPv6, False otherwise. """ - try: - import httpx2 as httpx - except ModuleNotFoundError: - import httpx + import httpx2 try: - httpx.head("http://[2606:4700:4700::1111]", timeout=3) - except httpx.RequestError: + httpx2.head("http://[2606:4700:4700::1111]", timeout=3) + except httpx2.RequestError: return False else: return True @@ -162,15 +153,11 @@ def _httpx_client(): # Resolve the active HTTP library at call time. Prefer httpx2 when # available, fall back to real httpx on Python 3.8/3.9 (which httpx2 # cannot run on). Bind the classes to local names so pyright does not - # infer a union of `httpx2.HTTPTransport | httpx.HTTPTransport` — + # infer a union of `httpx2.HTTPTransport | httpx2.HTTPTransport` — # that union is not assignable to `Client(mounts=...)` because the # two transport classes are unrelated. - try: - import httpx2 - from httpx2._utils import get_environment_proxies - except ModuleNotFoundError: - import httpx as httpx2 # noqa: F401 — local name `httpx2` bound to the real httpx - from httpx._utils import get_environment_proxies # noqa: F811 + import httpx2 + from httpx2._utils import get_environment_proxies verify_setting = _httpx_verify_kwarg() # `httpx2` is a union of the two modules here (httpx2 in the try diff --git a/reflex/utils/registry.py b/reflex/utils/registry.py index b34dbc16b9d..589dc6bbeeb 100644 --- a/reflex/utils/registry.py +++ b/reflex/utils/registry.py @@ -20,14 +20,11 @@ def latency(registry: str) -> int: Returns: int: The latency of the registry in microseconds. """ - try: - import httpx2 as httpx - except ModuleNotFoundError: - import httpx + import httpx2 try: time_to_respond = net.get(registry, timeout=2).elapsed.microseconds - except httpx.HTTPError: + except httpx2.HTTPError: logger.info(f"Failed to connect to {registry}.") return 10_000_000 else: diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index 334b15cebf9..11df4ef479d 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -435,13 +435,10 @@ def _prepare_event( def _send_event(event_data: _Event) -> bool: - try: - import httpx2 as httpx - except ModuleNotFoundError: - import httpx + import httpx2 try: - httpx.post(POSTHOG_API_URL, json=event_data) + httpx2.post(POSTHOG_API_URL, json=event_data) except Exception: return False else: diff --git a/reflex/utils/templates.py b/reflex/utils/templates.py index e0203ec4b3e..f7ef30518ae 100644 --- a/reflex/utils/templates.py +++ b/reflex/utils/templates.py @@ -122,10 +122,7 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str SystemExit: If any download, file operations fail or unexpected zip file format. """ - try: - import httpx2 as httpx - except ModuleNotFoundError: - import httpx + import httpx2 # Create a temp directory for the zip download. try: @@ -141,7 +138,7 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str response = net.get(template_url, follow_redirects=True) logger.debug(f"Server responded download request: {response}") response.raise_for_status() - except httpx.HTTPError as he: + except httpx2.HTTPError as he: logger.error(f"Failed to download the template: {he}") raise SystemExit(1) from None try: diff --git a/tests/test_node_version.py b/tests/test_node_version.py index 6f5ea6d15c3..5c1341d23c6 100644 --- a/tests/test_node_version.py +++ b/tests/test_node_version.py @@ -5,7 +5,7 @@ from collections.abc import Generator from typing import Any -import httpx +import httpx2 import pytest from playwright.sync_api import Page, expect @@ -58,7 +58,7 @@ def test_node_version(node_version_app: AppHarness, page: Page): """ def get_latest_node_version(): - response = httpx.get("https://nodejs.org/dist/index.json") + response = httpx2.get("https://nodejs.org/dist/index.json") versions = response.json() # Assuming the first entry in the API response is the most recent version diff --git a/tests/units/test_telemetry.py b/tests/units/test_telemetry.py index 7ee3c8c0985..170b6c0abd1 100644 --- a/tests/units/test_telemetry.py +++ b/tests/units/test_telemetry.py @@ -66,11 +66,9 @@ def httpx_post(mocker: MockerFixture): Returns: The mock for ``httpx.post`` so tests can assert on the posted payload. """ - try: - import httpx2 as httpx - except ModuleNotFoundError: - import httpx - return mocker.patch.object(httpx, "post") + import httpx2 + + return mocker.patch.object(httpx2, "post") def test_telemetry(): @@ -124,7 +122,7 @@ def test_get_reflex_package_versions_reports_only_first_party(mocker: MockerFixt "reflex-base>=0.9.4", "reflex-components-radix>=0.9.2", "reflex-hosting-cli>=0.1.66", - "httpx<1.0,>=0.26", + "httpx2>=2.12.0", 'pydantic>=2.12.0; extra == "db"', ], ) @@ -132,7 +130,7 @@ def test_get_reflex_package_versions_reports_only_first_party(mocker: MockerFixt "reflex-base": "0.9.4", "reflex-components-radix": "0.9.2", # reflex-hosting-cli is a declared dependency but is not installed here. - "httpx": "0.27.0", + "httpx2": "2.12.0", "pydantic": "2.12.0", # A third-party reflex-* package installed separately by the user. "reflex-enterprise": "1.2.3", diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index d643124c5a4..cbb406318ad 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -537,14 +537,11 @@ def test_initialize_agents_md_refreshes_managed_section(tmp_path, mocker): def test_initialize_agents_md_warns_on_fetch_failure(tmp_path, mocker, caplog): """Test that a failed fetch warns without writing AGENTS.md or the bridge.""" - try: - import httpx2 as httpx - except ModuleNotFoundError: - import httpx + import httpx2 agents_file = tmp_path / "AGENTS.md" claude_file = tmp_path / "CLAUDE.md" - mocker.patch("reflex.utils.net.get", side_effect=httpx.ConnectError("boom")) + mocker.patch("reflex.utils.net.get", side_effect=httpx2.ConnectError("boom")) frontend_skeleton.initialize_agents_md( agents_file=agents_file, claude_file=claude_file diff --git a/uv.lock b/uv.lock index 034b43b0cf9..f7ab181dcb0 100644 --- a/uv.lock +++ b/uv.lock @@ -15,7 +15,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-08-27T03:10:58.998873Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P7D" [options.exclude-newer-package] @@ -659,7 +659,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -1068,7 +1068,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1993,15 +1993,15 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "cycler", marker = "python_full_version < '3.11'" }, - { name = "fonttools", marker = "python_full_version < '3.11'" }, - { name = "kiwisolver", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pillow", marker = "python_full_version < '3.11'" }, - { name = "pyparsing", marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" } }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } wheels = [ @@ -2659,10 +2659,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -3766,7 +3766,6 @@ source = { editable = "." } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, - { name = "httpx" }, { name = "httpx2" }, { name = "packaging" }, { name = "psutil", marker = "sys_platform == 'win32'" }, @@ -3802,6 +3801,11 @@ db = [ pydantic = [ { name = "reflex-base", extra = ["pydantic"] }, ] +testing = [ + { name = "psutil" }, + { name = "selenium" }, + { name = "uvicorn" }, +] [package.dev-dependencies] dev = [ @@ -3858,10 +3862,10 @@ requires-dist = [ { name = "alembic", marker = "extra == 'db'", specifier = ">=1.15.2,<2.0" }, { name = "click", specifier = ">=8.2" }, { name = "granian", extras = ["reload"], specifier = ">=2.7.4" }, - { name = "httpx", specifier = ">=0.26,<1.0" }, - { name = "httpx2", marker = "python_full_version >= '3.10'", specifier = ">=2.0" }, + { name = "httpx2", specifier = ">=2.12.0" }, { name = "packaging", specifier = ">=24.2,<27" }, { name = "psutil", marker = "sys_platform == 'win32'", specifier = ">=7.0.0,<8.0" }, + { name = "psutil", marker = "extra == 'testing'", specifier = ">=7.0.0,<8.0" }, { name = "pydantic", marker = "extra == 'db'", specifier = ">=2.12.0,<3.0" }, { name = "python-multipart", specifier = ">=0.0.32,<1.0" }, { name = "python-socketio", specifier = ">=5.12.0,<6.0" }, @@ -3882,12 +3886,14 @@ requires-dist = [ { name = "reflex-components-sonner", editable = "packages/reflex-components-sonner" }, { name = "reflex-hosting-cli", editable = "packages/reflex-hosting-cli" }, { name = "rich", specifier = ">=13,<16" }, + { name = "selenium", marker = "extra == 'testing'", specifier = ">=4.0.0,<5.0" }, { name = "sqlmodel", marker = "extra == 'db'", specifier = ">=0.0.24,<0.1" }, { name = "starlette", specifier = ">=1.3.1" }, { name = "typing-extensions", specifier = ">=4.13.0" }, + { name = "uvicorn", marker = "extra == 'testing'", specifier = ">=0.34.0,<1.0" }, { name = "wrapt", specifier = ">=1.17.0,<2.4" }, ] -provides-extras = ["db", "pydantic"] +provides-extras = ["db", "pydantic", "testing"] [package.metadata.requires-dev] dev = [ @@ -4433,7 +4439,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -4494,7 +4500,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -4999,15 +5005,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, ] -[[package]] -name = "ty" -version = "0.0.69" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, -] - [[package]] name = "ty" version = "0.0.73" From bccf225f713b8eb2115ccf3518a9b940316a90c7 Mon Sep 17 00:00:00 2001 From: xic Date: Thu, 10 Sep 2026 14:45:01 +0800 Subject: [PATCH 4/4] fix: sync uv.lock with manifest, add news fragment Remove a stale 'testing' extra block from the reflex package entry in uv.lock (carried over from upstream's lock; the branch manifest only declares db/pydantic extras), and add news/7040.breaking.md per the repo's changelog requirement. Addresses greptile review findings. --- news/7040.breaking.md | 1 + uv.lock | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) create mode 100644 news/7040.breaking.md diff --git a/news/7040.breaking.md b/news/7040.breaking.md new file mode 100644 index 00000000000..7a590369423 --- /dev/null +++ b/news/7040.breaking.md @@ -0,0 +1 @@ +Replace the runtime HTTP dependency `httpx` with `httpx2` (the actively maintained fork). TLS verification now uses the OS trust store instead of the bundled `certifi` CA bundle; deployments behind corporate proxies or in minimal containers may need `SSL_CERT_FILE` / `SSL_CERT_DIR` set. diff --git a/uv.lock b/uv.lock index f7ab181dcb0..7f47baccc64 100644 --- a/uv.lock +++ b/uv.lock @@ -3801,12 +3801,6 @@ db = [ pydantic = [ { name = "reflex-base", extra = ["pydantic"] }, ] -testing = [ - { name = "psutil" }, - { name = "selenium" }, - { name = "uvicorn" }, -] - [package.dev-dependencies] dev = [ { name = "alembic" },