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 docs/howto/upgrade.rst
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ network errors and server errors (HTTP 500, 502, 503, or 504) are considered
retryable. You can customize this behavior with the ``process_exception``
argument of :func:`~asyncio.client.connect`.

See :func:`~asyncio.client.process_exception` for more information.
See :func:`~client.process_exception` for more information.

Here's how to revert to the behavior of the original implementation::

Expand Down
13 changes: 13 additions & 0 deletions docs/project/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ notice.

*In development*

New features
............

* Added support for reconnecting automatically by using
:func:`~sync.client.reconnect` as an iterator to the :mod:`threading`
implementation.

* :func:`~sync.client.connect` now follows redirects in the :mod:`threading`
implementation.

* :func:`~sync.client.connect` can connect to another host and port than those
specified in the URI in the :mod:`threading` implementation.

.. _17.0.1:

17.0.1
Expand Down
2 changes: 0 additions & 2 deletions docs/reference/asyncio/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ Opening a connection
.. autofunction:: unix_connect
:async:

.. autofunction:: process_exception

Using a connection
------------------

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ Client
+------------------------------------+--------+--------+--------+--------+--------+
| Close connection on context exit | ✅ | ✅ | ✅ | — | ✅ |
+------------------------------------+--------+--------+--------+--------+--------+
| Reconnect automatically | ✅ | | ✅ | — | ✅ |
| Reconnect automatically | ✅ | | ✅ | — | ✅ |
+------------------------------------+--------+--------+--------+--------+--------+
| Configure ``Origin`` header | ✅ | ✅ | ✅ | ✅ | ✅ |
+------------------------------------+--------+--------+--------+--------+--------+
Expand All @@ -161,7 +161,7 @@ Client
+------------------------------------+--------+--------+--------+--------+--------+
| Connect to non-ASCII IRIs | ✅ | ✅ | ✅ | ✅ | ✅ |
+------------------------------------+--------+--------+--------+--------+--------+
| Follow HTTP redirects | ✅ | | ✅ | — | ✅ |
| Follow HTTP redirects | ✅ | | ✅ | — | ✅ |
+------------------------------------+--------+--------+--------+--------+--------+
| Perform HTTP Basic Authentication | ✅ | ✅ | ✅ | ✅ | ✅ |
+------------------------------------+--------+--------+--------+--------+--------+
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/sansio/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ Client (`Sans-I/O`_)
.. autoproperty:: close_reason

.. autoproperty:: close_exc

.. autofunction:: process_exception
4 changes: 4 additions & 0 deletions docs/reference/sync/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ Opening a connection

.. autofunction:: connect

.. autofunction:: reconnect

.. autofunction:: unix_connect

.. autofunction:: unix_reconnect

Using a connection
------------------

Expand Down
2 changes: 0 additions & 2 deletions docs/reference/trio/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ Opening a connection
.. autofunction:: unix_connect
:async:

.. autofunction:: process_exception

Using a connection
------------------

Expand Down
75 changes: 15 additions & 60 deletions src/websockets/asyncio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
from types import TracebackType
from typing import Any, Callable, Literal, cast

from ..client import ClientProtocol, backoff
from ..client import ClientProtocol, backoff, process_exception
from ..datastructures import Headers, HeadersLike
from ..exceptions import (
InvalidMessage,
InvalidProxyMessage,
InvalidProxyStatus,
InvalidStatus,
Expand Down Expand Up @@ -128,59 +127,14 @@ def process_event(self, event: Event) -> None:
super().process_event(event)


def process_exception(exc: Exception) -> Exception | None:
"""
Determine whether a connection error is retryable or fatal.

When reconnecting automatically with ``async for ... in connect(...)``, if a
connection attempt fails, :func:`process_exception` is called to determine
whether to retry connecting or to raise the exception.

This function defines the default behavior, which is to retry on:

* :exc:`EOFError`, :exc:`OSError`, :exc:`asyncio.TimeoutError`: network
errors;
* :exc:`~websockets.exceptions.InvalidStatus` when the status code is 500,
502, 503, or 504: server or proxy errors.

All other exceptions are considered fatal.

You can change this behavior with the ``process_exception`` argument of
:func:`connect`.

Return :obj:`None` if the exception is retryable i.e. when the error could
be transient and trying to reconnect with the same parameters could succeed.
The exception will be logged at the ``INFO`` level.

Return an exception, either ``exc`` or a new exception, if the exception is
fatal i.e. when trying to reconnect will most likely produce the same error.
That exception will be raised, breaking out of the retry loop.

"""
# This catches python-socks' ProxyConnectionError and ProxyTimeoutError.
if isinstance(exc, (OSError, TimeoutError)):
return None
if isinstance(exc, InvalidMessage) and isinstance(exc.__cause__, EOFError):
return None
if isinstance(exc, InvalidStatus) and exc.response.status_code in [
500, # Internal Server Error
502, # Bad Gateway
503, # Service Unavailable
504, # Gateway Timeout
]:
return None
return exc


# This is spelled in lower case because it's exposed as a callable in the API.
class connect:
"""
Connect to the WebSocket server at ``uri``.

This coroutine returns a :class:`ClientConnection` instance, which you can
use to send and receive messages.

:func:`connect` may be used as an asynchronous context manager::
:func:`connect` should be treated as an asynchronous context manager
yielding a :class:`ClientConnection`, which can then receive and send
messages::

from websockets.asyncio.client import connect

Expand All @@ -189,8 +143,8 @@ class connect:

The connection is closed automatically when exiting the context.

:func:`connect` can be used as an infinite asynchronous iterator to
reconnect automatically on errors::
:func:`connect` can also be treated as an infinite asynchronous iterator
to reconnect automatically on errors::

async for websocket in connect(...):
try:
Expand Down Expand Up @@ -225,7 +179,8 @@ class connect:
<../../topics/proxies>` for details.
process_exception: When reconnecting automatically, tell whether an
error is transient or fatal. The default behavior is defined by
:func:`process_exception`. Refer to its documentation for details.
:func:`~websockets.client.process_exception`. Refer to its
documentation for details.
open_timeout: Timeout for opening the connection in seconds.
:obj:`None` disables the timeout.
ping_interval: Interval between keepalive pings in seconds.
Expand Down Expand Up @@ -555,13 +510,7 @@ def process_redirect(self, exc: Exception) -> Exception | str:

return new_uri

# ... = await connect(...)

def __await__(self) -> Generator[Any, None, ClientConnection]:
# Create a suitable iterator by calling __await__ on a coroutine.
return self.__await_impl__().__await__()

async def __await_impl__(self) -> ClientConnection:
async def connect(self) -> ClientConnection:
try:
async with asyncio.timeout(self.open_timeout):
for _ in range(MAX_REDIRECTS):
Expand Down Expand Up @@ -606,6 +555,12 @@ async def __await_impl__(self) -> ClientConnection:
# Re-raise exception with an informative error message.
raise TimeoutError("timed out during opening handshake") from exc

# ... = await connect(...)

def __await__(self) -> Generator[Any, None, ClientConnection]:
# Create a suitable iterator by calling __await__ on a coroutine.
return self.connect().__await__()

# async with connect(...) as ...: ...

async def __aenter__(self) -> ClientConnection:
Expand Down
47 changes: 47 additions & 0 deletions src/websockets/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,53 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)


def process_exception(exc: Exception) -> Exception | None:
"""
Determine whether a connection error is retryable or fatal.

When reconnecting automatically with ``async for ... in connect(...)`` in
the :mod:`asyncio` and :mod:`trio` implementations or with ``for ... in
reconnect(...)`` in the :mod:`threading` implementation, if a connection
attempt fails, :func:`process_exception` is called to determine whether to
retry connecting or to raise the exception.

This function defines the default behavior, which is to retry on:

* :exc:`OSError` and :exc:`asyncio.TimeoutError`: network errors;
* :exc:`~websockets.exceptions.InvalidMessage` when it stems from an
:exc:`EOFError`: also network errors;
* :exc:`~websockets.exceptions.InvalidStatus` when the status code is 500,
502, 503, or 504: server or proxy errors.

All other exceptions are considered fatal.

You can change this behavior with the ``process_exception`` argument of
:func:`connect`.

Return :obj:`None` if the exception is retryable i.e. when the error could
be transient and trying to reconnect with the same parameters could succeed.
The exception will be logged at the ``INFO`` level.

Return an exception, either ``exc`` or a new exception, if the exception is
fatal i.e. when trying to reconnect will most likely produce the same error.
That exception will be raised, breaking out of the retry loop.

"""
# This catches python-socks' ProxyConnectionError and ProxyTimeoutError.
if isinstance(exc, (OSError, TimeoutError)):
return None
if isinstance(exc, InvalidMessage) and isinstance(exc.__cause__, EOFError):
return None
if isinstance(exc, InvalidStatus) and exc.response.status_code in [
500, # Internal Server Error
502, # Bad Gateway
503, # Service Unavailable
504, # Gateway Timeout
]:
return None
return exc


BACKOFF_INITIAL_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_INITIAL_DELAY", "5"))
BACKOFF_MIN_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_MIN_DELAY", "3.1"))
BACKOFF_MAX_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_MAX_DELAY", "90.0"))
Expand Down
Loading
Loading