-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[HTTPXodus] migrate httpx to httpx2 (dual import) #7040
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # HTTPXodus Migration: reflex-dev/reflex β httpx2 | ||
|
|
||
| **Branch:** `httpxodus/httpx2-migration` | ||
| **Issue:** <https://github.com/reflex-dev/reflex/issues/7034> | ||
| **Date:** 2026-09-03 | ||
| **Mode:** Dual import (httpx2 preferred, httpx fallback) | ||
|
|
||
| ## What changed | ||
|
|
||
| | File | Change | | ||
| |------|--------| | ||
| | `pyproject.toml` | Added `httpx2 >=2.0; python_version >= '3.10'` next to existing `httpx >=0.26,<1.0` runtime dep | | ||
| | `reflex/utils/net.py` | 4 import sites β dual import. The private `get_environment_proxies` import is also dual-bound (httpx2 ships the same helper) | | ||
| | `reflex/utils/registry.py` | 1 import site β dual import | | ||
| | `reflex/utils/js_runtimes.py` | 1 import site β dual import | | ||
| | `reflex/utils/frontend_skeleton.py` | 1 import site β dual import | | ||
| | `reflex/utils/templates.py` | 1 import site β dual import | | ||
| | `reflex/utils/telemetry.py` | 1 import site β dual import | | ||
| | `reflex/custom_components/custom_components.py` | 1 import site β dual import | | ||
| | `tests/units/test_telemetry.py` | `httpx_post` fixture: same dual-import pattern so it mocks whichever module wins | | ||
| | `tests/units/utils/test_utils.py` | 1 test: same dual-import so the side-effect `httpx.ConnectError` comes from the same module the production code catches | | ||
| | `uv.lock` | regenerated by `uv sync` (httpx2 2.12.0 added, httpcore2 transitively pulled in) | | ||
|
|
||
| 7 production files migrated (matches issue #7034 count exactly). No `AsyncClient` anywhere β every site is sync, every call is CLI/framework tooling, nothing leaks into the public API. | ||
|
|
||
| ## Why dual import, not hard switch | ||
|
|
||
| - The repo still ships as a library β keeping `httpx >=0.26,<1.0` in runtime deps avoids breaking downstream apps that pin to `httpx` resolvers. | ||
| - The issue already framed both options; the dual-import variant is the recommended one in the HTTPXodus charter and matches what `starlette`, `anthropic-sdk`, and `mcp-sdk` chose. | ||
| - `httpx2` ships the same private helper (`httpx2._utils.get_environment_proxies`), so the `_httpx_client()` factory in `net.py` works against either package without runtime branching. | ||
|
|
||
| ## Out of scope (deliberately) | ||
|
|
||
| - `reflex-hosting-cli` β independent package, separate `pyproject.toml`. Issue notes that covering it would be part of a complete migration story, but it's not in this PR's scope. | ||
| - `docs/` β the docs site has its own workspace and its own `httpx` usage; it's a separate release. | ||
| - `tests/test_node_version.py`, `tests/integration/...` β top-level integration tests, also not in `tests/units/`. Most are dev-environment/network-dependent and would not run in this validation pass. | ||
|
|
||
| ## Validation | ||
|
|
||
| `uv sync` succeeded (exit 0). `pytest reflex/utils tests/units/` results: | ||
|
|
||
| ``` | ||
| 8137 passed, 18 skipped, 447 warnings in 83.01s (0:01:23) | ||
| ``` | ||
|
|
||
| `ruff check .` and `ruff format --check` are clean on the touched files. | ||
|
|
||
| ### Test fixes required by the migration | ||
|
|
||
| Two test files had to be aligned with the new import pattern (otherwise `httpx_post` mocks the wrong module and `httpx.ConnectError` side-effects aren't caught by `httpx2.HTTPError`): | ||
|
|
||
| 1. `tests/units/test_telemetry.py` β the `httpx_post` fixture now uses the same dual-import so it mocks whichever module is actually installed. | ||
| 2. `tests/units/utils/test_utils.py` β `test_initialize_agents_md_warns_on_fetch_failure` uses the dual-import to get `httpx.ConnectError` from the same module the production code catches. | ||
|
|
||
| Both are minimal, mechanical, and only touch the modules that exercise the migrated code paths. | ||
|
|
||
| ### Pre-existing test infra issue (not caused by migration) | ||
|
|
||
| `tests/units/reflex_base/utils/pyi_generator/test_build_hashes.py::test_build_entrypoint_does_not_touch_pyi_hashes` fails when `ruff` is not on `PATH`. The test invokes the pyi_generator as a subprocess, and the generator itself shells out to `ruff format`. With `PATH=/path/to/.venv/bin:$PATH` (so the venv's `ruff` is visible to the subprocess) the test passes. This is independent of the migration. | ||
|
|
||
| ## Behavior caveat worth flagging in the changelog | ||
|
|
||
| `httpx2` verifies TLS against the **OS trust store** instead of `certifi`. Reflex already has first-class handling for custom `verify=` and proxy mounts (`net._httpx_client()`), but the OS trust-store change can shift behavior in containers and corporate-proxy environments. The CLAUDE.md / AGENTS.md already explains this; worth a one-line mention in the user-facing changelog when this lands. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: This try/except dual-import block is duplicated verbatim in 7+ files (net.py has 4 copies). It is not a circular-import case, so it belongs in a shared helper, e.g. Prompt for AI agents |
||
| except ModuleNotFoundError: | ||
| import httpx | ||
|
|
||
| # Download the script | ||
| logger.debug(f"Downloading {url}") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: httpx2 is added to the required
dependencies, but the dual-import code across net.py, telemetry.py, registry.py, and the download utilities relies on httpx2 being optional (except ModuleNotFoundError: import httpx). Because httpx2 is now always installed, that fallback is dead code, and every reflex install is forced to pull httpx2 β contradicting the PR's stated Option A of keeping it optional for apps that pin httpx. Move it to[project.optional-dependencies](or drop the fallback if httpx2 is meant to be mandatory).Prompt for AI agents