diff --git a/pyproject.toml b/pyproject.toml index 7fa2954d18c..b12018d16de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ requires-python = ">=3.10,<4.0" dependencies = [ "click >=8.2", "granian[reload] >=2.7.4", - "httpx >=0.26,<1.0", + "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 35dd3bcb4da..3f5080ca280 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -634,7 +634,7 @@ def _collect_details_for_gallery(): Raises: SystemExit: If pyproject.toml file is ill-formed or the request to the backend services fails. """ - import httpx + import httpx2 from reflex_cli.utils import hosting console.rule("[bold]Authentication with Reflex Services") @@ -666,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 @@ -704,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, @@ -713,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 9a5fa3d9ee3..e899cd1a3f8 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -141,13 +141,13 @@ def initialize_agents_md( """ plan = _plan_agents_md(agents_file, claude_file) - 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 2f5b9a5188d..f1a2127fe90 100644 --- a/reflex/utils/js_runtimes.py +++ b/reflex/utils/js_runtimes.py @@ -234,14 +234,14 @@ def download_and_run(url: str, *args, show_status: bool = False, **env): Raises: SystemExit: If the script fails to download. """ - 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 ffa96b95ce3..7938087b2db 100644 --- a/reflex/utils/net.py +++ b/reflex/utils/net.py @@ -41,14 +41,14 @@ def _wrap_https_func( @functools.wraps(func) def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T: - 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( @@ -95,11 +95,11 @@ def _is_ipv4_supported() -> bool: Returns: True if the system supports IPv4, False otherwise. """ - 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 @@ -111,11 +111,11 @@ def _is_ipv6_supported() -> bool: Returns: True if the system supports IPv6, False otherwise. """ - 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 @@ -150,12 +150,25 @@ def _httpx_client(): Returns: An HTTPX client. """ - import httpx - from httpx._utils import get_environment_proxies + # 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 | httpx2.HTTPTransport` — + # that union is not assignable to `Client(mounts=...)` because the + # two transport classes are unrelated. + import httpx2 + from httpx2._utils import get_environment_proxies 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, ), @@ -163,8 +176,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() @@ -172,4 +186,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] diff --git a/reflex/utils/registry.py b/reflex/utils/registry.py index 994f7ea1ae0..589dc6bbeeb 100644 --- a/reflex/utils/registry.py +++ b/reflex/utils/registry.py @@ -20,11 +20,11 @@ def latency(registry: str) -> int: Returns: int: The latency of the registry in microseconds. """ - 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 a1d76d2e8b5..11df4ef479d 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -435,10 +435,10 @@ def _prepare_event( def _send_event(event_data: _Event) -> bool: - 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 f47976e28ad..f7ef30518ae 100644 --- a/reflex/utils/templates.py +++ b/reflex/utils/templates.py @@ -122,7 +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. """ - import httpx + import httpx2 # Create a temp directory for the zip download. try: @@ -138,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 aa530e39ccf..170b6c0abd1 100644 --- a/tests/units/test_telemetry.py +++ b/tests/units/test_telemetry.py @@ -66,7 +66,9 @@ def httpx_post(mocker: MockerFixture): Returns: The mock for ``httpx.post`` so tests can assert on the posted payload. """ - return mocker.patch("httpx.post") + import httpx2 + + return mocker.patch.object(httpx2, "post") def test_telemetry(): @@ -120,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"', ], ) @@ -128,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 98ebe80475b..cbb406318ad 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -537,11 +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.""" - 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 43f7d18e4d3..f7ab181dcb0 100644 --- a/uv.lock +++ b/uv.lock @@ -3766,7 +3766,7 @@ source = { editable = "." } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, - { name = "httpx" }, + { name = "httpx2" }, { name = "packaging" }, { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "python-multipart" }, @@ -3801,6 +3801,11 @@ db = [ pydantic = [ { name = "reflex-base", extra = ["pydantic"] }, ] +testing = [ + { name = "psutil" }, + { name = "selenium" }, + { name = "uvicorn" }, +] [package.dev-dependencies] dev = [ @@ -3857,9 +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", 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" }, @@ -3880,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 = [