Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
b1363ca
Boostrap project
frankie567 Nov 22, 2022
0e91d2b
Start implementing API and ASGI transport
frankie567 Nov 22, 2022
ecf30b6
Improve transport exceptions
frankie567 Nov 22, 2022
3499bbb
Add test for upgrade error
frankie567 Nov 22, 2022
2b673d4
Change API so base methods work with pure wsproto Event
frankie567 Nov 22, 2022
4a815b0
Add helper send methods
frankie567 Nov 22, 2022
a5d8972
Add receive methods helpers
frankie567 Nov 22, 2022
aa636c6
Add sync version of the API
frankie567 Nov 22, 2022
35480c6
Revamp unit tests so they work against a real Uvicorn server
frankie567 Nov 23, 2022
f8a96d2
Add tests for ASGIWebSocketTransport
frankie567 Nov 23, 2022
1c704db
Try to use Unix socket to serve apps during testing
frankie567 Nov 23, 2022
fd98e0c
Add max_bytes parameter to all receive methods
frankie567 Nov 23, 2022
5e11fc9
Swallow WebSocketDisconnect during unit tests
frankie567 Nov 23, 2022
dd6b986
Improve test Uvicorn servers so they shutdown gracefully
frankie567 Nov 23, 2022
748189a
Tweak conftest
frankie567 Nov 24, 2022
0cad9bb
Tweak close methods
frankie567 Nov 24, 2022
0c10cfc
Test against uvicorn websockets impl
frankie567 Nov 24, 2022
46955b6
Tweak events handling
frankie567 Nov 24, 2022
6c240a4
Fix unit tests by adding sleeps
frankie567 Nov 24, 2022
f559407
Improve API
frankie567 Nov 24, 2022
0908424
Bump version 0.0.0 → 0.1.0
frankie567 Nov 24, 2022
d948b56
#2 Add ping method (#8)
kousikmitra Nov 24, 2022
56b010b
Fix test name
frankie567 Nov 25, 2022
96ef50d
Bump version 0.1.0 → 0.1.1
frankie567 Nov 25, 2022
3785e2a
Implement auto ping respond
frankie567 Nov 25, 2022
0b57312
Revamp receive implementation with a background thread
frankie567 Nov 25, 2022
8189b6d
Implement ping-pong handling
frankie567 Nov 25, 2022
b49462f
Add timeout handling for async receive
frankie567 Nov 25, 2022
765bd2c
Add parameters to control message and queue size
frankie567 Nov 25, 2022
5c00b25
Add a sleep in transport receive to let it yield to the event loop
frankie567 Nov 25, 2022
1c63294
Add docstrings for session classes
frankie567 Nov 27, 2022
d72fef5
Complete docs
frankie567 Nov 27, 2022
a02695d
Bump version 0.1.1 → 0.2.0
frankie567 Nov 27, 2022
08e5848
Ensure to close stream when calling close
frankie567 Nov 27, 2022
0677904
Make sure some unit tests correctly close the websocket session
frankie567 Nov 27, 2022
9d7653d
Greatly improve network errors handling
frankie567 Nov 27, 2022
6f666fa
Add missing unit tests
frankie567 Nov 27, 2022
722bcc2
Handle immediate close in ASGI transport
frankie567 Nov 28, 2022
8f77e6a
Bump version 0.2.0 → 0.2.1
frankie567 Nov 28, 2022
0e015ef
Add automatic keepalive ping mechanism
frankie567 Nov 28, 2022
ab338e1
Bump version 0.2.1 → 0.2.2
frankie567 Nov 28, 2022
82577fb
Add mechanism to stop thread early when they're stuck in a waiting op…
frankie567 Nov 29, 2022
dd95a86
Bump version 0.2.2 → 0.2.3
frankie567 Nov 29, 2022
28811b9
Fix #15: make sure default HTTPX client is closed (#16)
frankie567 Dec 27, 2022
66445dc
Bump version 0.2.3 → 0.2.4
frankie567 Dec 27, 2022
d1d7121
Fix #19: when both todo task and wait task are done in _wait_until_cl…
frankie567 Jan 2, 2023
e5a53a2
Fix typings
frankie567 Jan 2, 2023
aed282e
Bump version 0.2.4 → 0.2.5
frankie567 Jan 2, 2023
1fc1ed0
Fix case where we try to schedule tasks in new threadpool during clie…
frankie567 Jan 2, 2023
f86992f
Bump version 0.2.5 → 0.2.6
frankie567 Jan 3, 2023
44b9e0c
Add support for subprotocols (#24)
frankie567 Feb 15, 2023
1093bba
Bump version 0.2.6 → 0.3.0
frankie567 Feb 15, 2023
99fc8ee
Fix #30: handle server error in ASGI transport
frankie567 Apr 15, 2023
4975b76
Remove useless docstring on event_loop fixture
frankie567 Apr 15, 2023
e62c81d
Bump version 0.3.0 → 0.3.1
frankie567 Apr 15, 2023
f895896
Drop Python 3.7 support
frankie567 Jun 27, 2023
77e8356
Use Starlette lifespan instead of on_startup
frankie567 Jun 27, 2023
231736a
Bump version 0.3.1 → 0.4.0
frankie567 Jun 27, 2023
7fee7d3
Fixes `httpcore` import in `httpx_ws/_api.py` (#36)
saforem2 Jul 6, 2023
5c16606
Bump version 0.4.0 → 0.4.1
frankie567 Jul 6, 2023
b3d2f08
Fix #40: handle large message buffering (#44)
frankie567 Aug 8, 2023
45a6617
start_blocking_portal was moved in anyio 4 (#48)
maparent Sep 7, 2023
b3f7885
Fix some wsproto imports
frankie567 Sep 7, 2023
ba606bf
Fix #34: handle subprotocols corrrectly in `ASGIWebSocketTransport`
frankie567 Sep 27, 2023
03c17d5
Bump version 0.4.1 → 0.4.2
frankie567 Sep 27, 2023
142c205
Fix #56: return proper accept response headers with ASGIWebSocketTran…
frankie567 Nov 16, 2023
c4e1b65
Fix pytest-asyncio warning
frankie567 Dec 4, 2023
0812346
Bump version 0.4.2 → 0.4.3
frankie567 Dec 4, 2023
c4f053d
Implement AsyncWebSocketSession with AnyIO
frankie567 Feb 8, 2024
35fd642
Make WebSocketSession a context manager to mirror the async implement…
frankie567 Feb 8, 2024
4592cf8
Add flaky marker on test_async_keepalive_ping
frankie567 Feb 9, 2024
45f521b
Update docs
frankie567 Feb 9, 2024
8452893
Bump version 0.4.3 → 0.5.0
frankie567 Feb 9, 2024
4306a20
Upgrade httpcore>=1.0.4 and remove connection hang workaround in tests
frankie567 Feb 22, 2024
d272070
Increase flakiness of test_async_keepalive_ping
frankie567 Feb 22, 2024
3c03a67
Disable automatic keepalive ping when using ASGI transport
frankie567 Feb 22, 2024
7aa4a37
Bump version 0.5.0 → 0.5.1
frankie567 Feb 22, 2024
1a5f10b
Fix test_receive_oversized_message unit test
frankie567 Feb 22, 2024
87d25f4
Upgrade and fix Ruff linting
frankie567 Mar 19, 2024
2d99bf9
Bump version 0.5.1 → 0.5.2
frankie567 Mar 19, 2024
8b7d0e7
Add `response` attribute for `WebSocketSession`
WSH032 Oct 30, 2023
afb0802
Add test for attribute `response`
WSH032 Oct 30, 2023
3b25adf
parse subprotocol in `WebSocketSession`, rather than in `connect_ws`
WSH032 Apr 3, 2024
6fa05a4
Bump version 0.5.2 → 0.6.0
frankie567 Apr 5, 2024
41ce91a
Fix typing
frankie567 May 2, 2024
fae27f6
Don't catch cancellation exceptions
agronholm Jul 14, 2024
4ac6154
Fix linting
frankie567 Jul 15, 2024
ba71065
Merge pull request #73 from agronholm/no-catch-cancel
frankie567 Jul 15, 2024
8f1e355
Close memory object streams at exit
agronholm Jul 14, 2024
1a040db
Merge pull request #74 from agronholm/close-memoryobjectstreams
frankie567 Jul 15, 2024
6d3d4cd
Add unit test to reproduce #70
frankie567 Apr 8, 2024
053fa42
Merge pull request #71 from frankie567/fix/70
frankie567 Jul 23, 2024
d0f2c8e
Prevent threads to hang and pile up in WebSocketSession
ro-oliveira95 Sep 23, 2024
9c0d465
Merge pull request #77 from ro-oliveira95/fix-hanging-threads
frankie567 Sep 23, 2024
7a9e7f9
Bump version 0.6.0 → 0.6.1
frankie567 Oct 5, 2024
1ca2bed
Improve _wait_until_closed() (#81)
davidbrochart Oct 7, 2024
e5bd84d
Bump version 0.6.1 → 0.6.2
frankie567 Oct 7, 2024
c3dcf21
Drop Python 3.8 support
frankie567 Oct 9, 2024
352a5d7
Vendor httpx-ws into httpx2._websockets
Kludex Jun 24, 2026
d5ed6a5
Adapt vendored httpx-ws package to httpx2
Kludex Jun 24, 2026
6c7b81d
Expose WebSocket support in the httpx2 public API
Kludex Jun 24, 2026
29150ed
Adapt httpx-ws tests to httpx2 and wire up test dependencies
Kludex Jun 24, 2026
6022321
Guard WebSocket entrypoints with an inline wsproto import check
Kludex Jun 24, 2026
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
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,23 @@ httpx2 = { workspace = true }

[dependency-groups]
dev = [
"httpx2[brotli,cli,http2,socks,zstd]",
"httpx2[brotli,cli,http2,socks,ws,zstd]",
"httpcore2[asyncio,trio,http2,socks]",
# Tests
"chardet==6.0.0.post1",
"coverage[toml]==7.10.6",
"cryptography==46.0.7",
"flaky>=3.8",
"pytest>=9.0.3",
"pytest-codspeed>=4.1.1",
"pytest-httpbin==2.0.0",
"pytest-trio==0.8.0",
"starlette>=0.49",
"trio==0.31.0",
"trio-typing==0.10.0",
"trustme==1.2.1",
"uvicorn>=0.35",
"websockets>=15",
"werkzeug>=3.1.6",
# Linting
"mypy==1.17.1",
Expand Down
48 changes: 45 additions & 3 deletions src/httpx2/httpx2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import typing as _typing

from .__version__ import __description__, __title__, __version__
from ._api import *
from ._auth import *
Expand All @@ -11,15 +13,28 @@
from ._types import *
from ._urls import *

if _typing.TYPE_CHECKING:
from ._websockets._api import AsyncWebSocketSession, WebSocketSession
from ._websockets._exceptions import (
HTTPXWSException,
WebSocketDisconnect,
WebSocketInvalidTypeReceived,
WebSocketNetworkError,
WebSocketUpgradeError,
)
from ._websockets._transport import ASGIWebSocketTransport

__all__ = [
"__description__",
"__title__",
"__version__",
"ASGITransport",
"ASGIWebSocketTransport",
"AsyncBaseTransport",
"AsyncByteStream",
"AsyncClient",
"AsyncHTTPTransport",
"AsyncWebSocketSession",
"Auth",
"BaseTransport",
"BasicAuth",
Expand All @@ -42,6 +57,7 @@
"HTTPError",
"HTTPStatusError",
"HTTPTransport",
"HTTPXWSException",
"InvalidURL",
"Limits",
"LocalProtocolError",
Expand Down Expand Up @@ -78,20 +94,37 @@
"UnsupportedProtocol",
"URL",
"USE_CLIENT_DEFAULT",
"websocket",
"WebSocketDisconnect",
"WebSocketInvalidTypeReceived",
"WebSocketNetworkError",
"WebSocketSession",
"WebSocketUpgradeError",
"WriteError",
"WriteTimeout",
"WSGITransport",
]


_WEBSOCKET_NAMES = {
"ASGIWebSocketTransport",
"AsyncWebSocketSession",
"HTTPXWSException",
"WebSocketDisconnect",
"WebSocketInvalidTypeReceived",
"WebSocketNetworkError",
"WebSocketSession",
"WebSocketUpgradeError",
}

__locals = locals()
for __name in __all__:
if not __name.startswith("__"):
if not __name.startswith("__") and __name not in _WEBSOCKET_NAMES:
setattr(__locals[__name], "__module__", "httpx2") # noqa


def __getattr__(name: str) -> object: # pragma: no cover
if name == "main":
def __getattr__(name: str) -> object:
if name == "main": # pragma: no cover
import warnings

warnings.warn(
Expand All @@ -104,4 +137,13 @@ def __getattr__(name: str) -> object: # pragma: no cover

return main

if name in _WEBSOCKET_NAMES:
from . import _websockets
from ._websockets._defaults import WS_EXTRA_INSTALL_MESSAGE

try:
return getattr(_websockets, name)
except ImportError:
raise ImportError(WS_EXTRA_INSTALL_MESSAGE)

raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
63 changes: 63 additions & 0 deletions src/httpx2/httpx2/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@
TimeoutTypes,
)
from ._urls import URL
from ._websockets._defaults import (
DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
DEFAULT_MAX_MESSAGE_SIZE_BYTES,
DEFAULT_QUEUE_SIZE,
)

if typing.TYPE_CHECKING:
import ssl # pragma: no cover

from ._websockets._api import WebSocketSession


__all__ = [
"delete",
Expand All @@ -34,6 +42,7 @@
"put",
"request",
"stream",
"websocket",
]


Expand Down Expand Up @@ -424,3 +433,57 @@ def delete(
timeout=timeout,
trust_env=trust_env,
)


@contextmanager
def websocket(
url: URL | str,
*,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
verify: ssl.SSLContext | str | bool = True,
trust_env: bool = True,
subprotocols: list[str] | None = None,
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
queue_size: int = DEFAULT_QUEUE_SIZE,
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
) -> Generator[WebSocketSession]:
"""
Open a WebSocket session.

The session is closed automatically when exiting the context manager.

```python
with httpx2.websocket("ws://localhost:8000/ws") as ws:
ws.send_text("Hello!")
message = ws.receive_text()
```

**Parameters**: See `httpx2.request` and `httpx2.Client.websocket`.
"""
with Client(
cookies=cookies,
proxy=proxy,
verify=verify,
timeout=timeout,
trust_env=trust_env,
) as client:
with client.websocket(
url,
params=params,
headers=headers,
auth=auth,
follow_redirects=follow_redirects,
subprotocols=subprotocols,
max_message_size_bytes=max_message_size_bytes,
queue_size=queue_size,
keepalive_ping_interval_seconds=keepalive_ping_interval_seconds,
keepalive_ping_timeout_seconds=keepalive_ping_timeout_seconds,
) as session:
yield session
91 changes: 91 additions & 0 deletions src/httpx2/httpx2/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,19 @@
)
from ._urls import URL, QueryParams
from ._utils import URLPattern, get_environment_proxies
from ._websockets._defaults import (
DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
DEFAULT_MAX_MESSAGE_SIZE_BYTES,
DEFAULT_QUEUE_SIZE,
WS_EXTRA_INSTALL_MESSAGE,
)

if typing.TYPE_CHECKING:
import ssl # pragma: no cover

from ._websockets._api import AsyncWebSocketSession, WebSocketSession

__all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"]

# The type annotation for @classmethod and context managers here follows PEP 484
Expand Down Expand Up @@ -845,6 +854,47 @@ def stream(
finally:
response.close()

@contextmanager
def websocket(
self,
url: URL | str,
*,
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
queue_size: int = DEFAULT_QUEUE_SIZE,
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
subprotocols: list[str] | None = None,
**kwargs: typing.Any,
) -> Generator[WebSocketSession]:
"""
Open a WebSocket session, using this client's configuration.

The session is closed automatically when exiting the context manager.

```python
with httpx2.Client() as client:
with client.websocket("ws://localhost:8000/ws") as ws:
ws.send_text("Hello!")
message = ws.receive_text()
```
"""
try:
from ._websockets._api import connect_ws
except ImportError:
raise ImportError(WS_EXTRA_INSTALL_MESSAGE)

with connect_ws(
str(url),
self,
max_message_size_bytes=max_message_size_bytes,
queue_size=queue_size,
keepalive_ping_interval_seconds=keepalive_ping_interval_seconds,
keepalive_ping_timeout_seconds=keepalive_ping_timeout_seconds,
subprotocols=subprotocols,
**kwargs,
) as session:
yield session

def send(
self,
request: Request,
Expand Down Expand Up @@ -1548,6 +1598,47 @@ async def stream(
finally:
await response.aclose()

@asynccontextmanager
async def websocket(
self,
url: URL | str,
*,
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
queue_size: int = DEFAULT_QUEUE_SIZE,
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
subprotocols: list[str] | None = None,
**kwargs: typing.Any,
) -> AsyncGenerator[AsyncWebSocketSession]:
"""
Open a WebSocket session, using this client's configuration.

The session is closed automatically when exiting the context manager.

```python
async with httpx2.AsyncClient() as client:
async with client.websocket("ws://localhost:8000/ws") as ws:
await ws.send_text("Hello!")
message = await ws.receive_text()
```
"""
try:
from ._websockets._api import aconnect_ws
except ImportError:
raise ImportError(WS_EXTRA_INSTALL_MESSAGE)

async with aconnect_ws(
str(url),
self,
max_message_size_bytes=max_message_size_bytes,
queue_size=queue_size,
keepalive_ping_interval_seconds=keepalive_ping_interval_seconds,
keepalive_ping_timeout_seconds=keepalive_ping_timeout_seconds,
subprotocols=subprotocols,
**kwargs,
) as session:
yield session

async def send(
self,
request: Request,
Expand Down
21 changes: 21 additions & 0 deletions src/httpx2/httpx2/_websockets/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 François Voron

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading