Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Required News Fragment Missing

This hard switch changes the runtime HTTP dependency and TLS trust behavior for downstream users, but it does not add a root-package news fragment. The repository requires user-facing changes to include a news/7040.<type>.md entry explaining what changed and what it means for users, so this requirement must be satisfied before merging.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

"packaging >=24.2,<27",
"psutil >=7.0.0,<8.0; sys_platform == 'win32'",
"python-multipart >=0.0.32,<1.0",
Expand Down
12 changes: 6 additions & 6 deletions reflex/custom_components/custom_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions reflex/utils/frontend_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions reflex/utils/js_runtimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
)
Expand Down
44 changes: 29 additions & 15 deletions reflex/utils/net.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -150,26 +150,40 @@ 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The comment block just above this import still describes behavior removed by this change: it says the code "fall[s] back to real httpx on Python 3.8/3.9" and that httpx2 is "a union of the two modules" via a try/except. This file now hard-imports httpx2, and pyproject.toml declares httpx2 >=2.0 for the full supported range (requires-python = ">=3.10"), so there is no fallback and no union. The stale text and the # type: ignore suppressions (added only to silence the union inference) will mislead future maintainers. Update the comments to state that httpx2 is the sole HTTP client, and drop the now-unneeded # type: ignore suppressions on the Client/HTTPTransport/Proxy calls now that httpx2 is no longer a union.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At reflex/utils/net.py, line 159:

<comment>The comment block just above this import still describes behavior removed by this change: it says the code "fall[s] back to real httpx on Python 3.8/3.9" and that `httpx2` is "a union of the two modules" via a try/except. This file now hard-imports httpx2, and `pyproject.toml` declares `httpx2 >=2.0` for the full supported range (`requires-python = ">=3.10"`), so there is no fallback and no union. The stale text and the `# type: ignore` suppressions (added only to silence the union inference) will mislead future maintainers. Update the comments to state that httpx2 is the sole HTTP client, and drop the now-unneeded `# type: ignore` suppressions on the Client/HTTPTransport/Proxy calls now that `httpx2` is no longer a union.</comment>

<file context>
@@ -165,12 +156,8 @@ def _httpx_client():
-    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
 
</file context>

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,
),
mounts={
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()
},
)


get = _wrap_https_lazy_func(lambda: _httpx_client().get)
get = _wrap_https_lazy_func(lambda: _httpx_client().get) # type: ignore[arg-type]
4 changes: 2 additions & 2 deletions reflex/utils/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions reflex/utils/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions reflex/utils/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_node_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions tests/units/test_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -120,15 +122,15 @@ 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"',
],
)
installed = {
"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",
Expand Down
4 changes: 2 additions & 2 deletions tests/units/utils/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.