From 0389b3cb20ece7e9c1b763c4ccd37e12f5c688de Mon Sep 17 00:00:00 2001 From: magi Date: Sun, 6 Sep 2026 21:39:18 +0530 Subject: [PATCH] fix(http): HTTPClient unusable out of the box, hardcoded http2=True connect() passed http2=True unconditionally to httpx.AsyncClient(...), which raises ImportError unless the optional 'h2' package is installed -- never declared as a velocix dependency anywhere (pyproject.toml or requirements.txt only ever list httpx). Every HTTPClient() call, even a trivial GET, crashed for anyone who only installed velocix's own declared dependencies. Found while building a real app that fetches a file by URL. Made http2 a constructor param defaulting to False (httpx's own default), so the common case works with just httpx installed; anyone who wants HTTP/2 can opt in and install h2 themselves. Added tests/test_http_client.py -- this class had zero test coverage before. 260/260 tests, mypy clean, ruff clean. --- tests/test_http_client.py | 41 +++++++++++++++++++++++++++++++++++++++ velocix/http/client.py | 9 ++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/test_http_client.py diff --git a/tests/test_http_client.py b/tests/test_http_client.py new file mode 100644 index 0000000..c5f9ec1 --- /dev/null +++ b/tests/test_http_client.py @@ -0,0 +1,41 @@ +"""HTTPClient had zero test coverage before this: connect() hardcoded +http2=True in the httpx.AsyncClient(...) call, which raises ImportError +unless the optional 'h2' package is installed -- never declared as a +dependency anywhere, so the default constructor was unusable out of the +box for anyone who only installed velocix's declared requirements. +""" + +import asyncio + +from velocix.http.client import HTTPClient + + +def _run(coro): + return asyncio.run(coro) + + +def test_default_client_connects_without_h2_installed(): + async def scenario(): + client = HTTPClient() + await client.connect() + assert not client.is_closed + await client.close() + + _run(scenario()) + + +def test_default_client_as_context_manager(): + async def scenario(): + async with HTTPClient() as client: + assert not client.is_closed + + _run(scenario()) + + +def test_http2_is_opt_in(): + async def scenario(): + client = HTTPClient(http2=False) + await client.connect() + await client.close() + + _run(scenario()) diff --git a/velocix/http/client.py b/velocix/http/client.py index 4f855ae..156239f 100644 --- a/velocix/http/client.py +++ b/velocix/http/client.py @@ -48,6 +48,7 @@ class HTTPClient: "_limits", "_verify_ssl", "_follow_redirects", + "_http2", ) def __init__( @@ -60,6 +61,7 @@ def __init__( max_keepalive: int = 20, verify_ssl: bool = True, follow_redirects: bool = True, + http2: bool = False, ) -> None: self._client: httpx.AsyncClient | None = None self._timeout = timeout @@ -71,6 +73,11 @@ def __init__( ) self._verify_ssl = verify_ssl self._follow_redirects = follow_redirects + # Opt-in: httpx raises ImportError from AsyncClient(http2=True) unless + # the optional 'h2' package is installed, so defaulting this to True + # made HTTPClient unusable out of the box for anyone who only + # installed velocix's declared dependencies (just httpx, no h2). + self._http2 = http2 async def __aenter__(self) -> "HTTPClient": await self.connect() @@ -94,7 +101,7 @@ async def connect(self) -> None: limits=self._limits, verify=self._verify_ssl, follow_redirects=self._follow_redirects, - http2=True, # Enable HTTP/2 support + http2=self._http2, ) async def close(self) -> None: