Skip to content
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

Retries for connection failures #784

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
48fe281
Add .sleep() to backends
florimondmanca Jan 18, 2020
6b3ae74
Add docs
florimondmanca Jan 18, 2020
24d45e0
Add retries
florimondmanca Jan 18, 2020
89d04cc
Refine docs, interfaces and implementation
florimondmanca Jan 18, 2020
9d48d70
Lint, fix tests
florimondmanca Jan 18, 2020
6cbce48
Drop custom retry limits
florimondmanca Jan 19, 2020
6e57f96
Add tests
florimondmanca Jan 19, 2020
919cc9f
Disallow retry-on-response (for now)
florimondmanca Jan 20, 2020
94d8412
Improve coverage
florimondmanca Jan 20, 2020
6f98785
Don't include initial request in delays iterator
florimondmanca Jan 20, 2020
00ba058
Update docs
florimondmanca Jan 20, 2020
0d436e3
Add sync retries
florimondmanca Jan 20, 2020
76a5b9c
Drop RetryLimits internal interface, rearrange tests
florimondmanca Jan 20, 2020
e7c9734
Merge branch 'master' into retries-step1
florimondmanca Jan 20, 2020
e9c1542
Switch to off-by-default retries
florimondmanca Jan 20, 2020
cd4ded1
Update docs
florimondmanca Jan 20, 2020
6570b9f
Add 'retries' to sync client
florimondmanca Jan 20, 2020
19a9007
Tweak stacktrace in sync case
florimondmanca Jan 20, 2020
5db9eb9
Lint
florimondmanca Jan 20, 2020
c9abd75
Add per-request retries
florimondmanca Jan 20, 2020
8217d20
Merge branch 'master' into retry-on-connection-failures
florimondmanca Jan 22, 2020
a3e6917
Refactor for...else construct
florimondmanca Jan 22, 2020
d3609c3
Drop DEFAULT_RETRIES_CONFIG for None, add disabling retries per-request
florimondmanca Jan 22, 2020
c435bde
Tweak examples for client-level retries
florimondmanca Jan 23, 2020
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
53 changes: 53 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,56 @@ If you do need to make HTTPS connections to a local server, for example to test
>>> r
Response <200 OK>
```

## Retries

Communicating with a peer over a network is by essence subject to errors. HTTPX provides built-in retry functionality to increase the resilience to connection issues.

Retries are disabled by default. When retries are enabled, HTTPX will retry sending the request up to the specified number of times. This behavior is restricted to **connection failures only**, i.e.:

* Failures to establish or acquire a connection (`ConnectTimeout`, `PoolTimeout`).
* Failures to keep the connection open (`NetworkError`).

!!! important
HTTPX will **NOT** retry on failures that aren't related to establishing or maintaining connections. This includes in particular:

* Errors related to data transfer, such as `ReadTimeout` or `ProtocolError`.
* HTTP error responses (4xx, 5xx), such as `429 Too Many Requests` or `503 Service Unavailable`.
florimondmanca marked this conversation as resolved.
Show resolved Hide resolved

If HTTPX could not get a response after the specified number of retries, a `TooManyRetries` exception is raised.

The delay between each retry is increased exponentially to prevent overloading the requested host.

### Enabling retries

You can enable retries for a given request:

```python
# Using the top-level API:
response = httpx.get("https://example.org", retries=3)

# Using a client instance:
with httpx.Client() as client:
response = client.get("https://example.org", retries=3)
```

Or enable them on a client instance, which results in the given `retries` being used as a default for requests made with this client:

```python
# Retry at most 3 times on connection failures everywhere.
httpx.Client(retries=3)
```

### Fine-tuning the retries configuration

When enabling retries, the `retries` argument can also be an `httpx.Retries()` instance. It accepts the following arguments:

* An integer, given as a required positional argument, representing the maximum number of connection failures to retry on.
* `backoff_factor` (optional), which defines the increase rate of the time to wait between retries. By default this is `0.2`, which corresponds to issuing a new request after `(0s, 0.2s, 0.4s, 0.8s, ...)`. (Note that most connection failures are immediately resolved by retrying, so HTTPX will always issue the initial retry right away.)

```python
# Retry at most 5 times on connection failures,
# and issue new requests after `(0s, 0.5s, 1s, 2s, 4s, ...)`.
retries = httpx.Retries(5, backoff_factor=0.5)
client = httpx.Client(retries=retries)
```
florimondmanca marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 6 additions & 1 deletion httpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .api import delete, get, head, options, patch, post, put, request, stream
from .auth import Auth, BasicAuth, DigestAuth
from .client import AsyncClient, Client
from .config import PoolLimits, Proxy, Timeout
from .config import PoolLimits, Proxy, Retries, Timeout
from .dispatch.asgi import ASGIDispatch
from .dispatch.wsgi import WSGIDispatch
from .exceptions import (
Expand All @@ -12,6 +12,7 @@
DecodingError,
HTTPError,
InvalidURL,
NetworkError,
NotRedirectResponse,
PoolTimeout,
ProtocolError,
Expand All @@ -25,6 +26,7 @@
StreamConsumed,
TimeoutException,
TooManyRedirects,
TooManyRetries,
WriteTimeout,
)
from .models import URL, Cookies, Headers, QueryParams, Request, Response
Expand Down Expand Up @@ -54,12 +56,15 @@
"PoolLimits",
"Proxy",
"Timeout",
"Retries",
"TooManyRetries",
"ConnectTimeout",
"CookieConflict",
"ConnectionClosed",
"DecodingError",
"HTTPError",
"InvalidURL",
"NetworkError",
"NotRedirectResponse",
"PoolTimeout",
"ProtocolError",
Expand Down
31 changes: 28 additions & 3 deletions httpx/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

from .auth import AuthTypes
from .client import Client, StreamContextManager
from .config import DEFAULT_TIMEOUT_CONFIG, CertTypes, TimeoutTypes, VerifyTypes
from .config import (
DEFAULT_RETRIES_CONFIG,
DEFAULT_TIMEOUT_CONFIG,
CertTypes,
RetriesTypes,
TimeoutTypes,
VerifyTypes,
)
from .models import (
CookieTypes,
HeaderTypes,
Expand All @@ -26,6 +33,7 @@ def request(
headers: HeaderTypes = None,
cookies: CookieTypes = None,
auth: AuthTypes = None,
retries: RetriesTypes = DEFAULT_RETRIES_CONFIG,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
allow_redirects: bool = True,
verify: VerifyTypes = True,
Expand Down Expand Up @@ -54,6 +62,8 @@ def request(
request.
* **auth** - *(optional)* An authentication class to use when sending the
request.
* **retries** - *(optional)* The maximum number of connection failures to
retry on.
* **timeout** - *(optional)* The timeout configuration to use when sending
the request.
* **allow_redirects** - *(optional)* Enables or disables HTTP redirects.
Expand Down Expand Up @@ -81,7 +91,7 @@ def request(
```
"""
with Client(
cert=cert, verify=verify, timeout=timeout, trust_env=trust_env,
cert=cert, verify=verify, retries=retries, timeout=timeout, trust_env=trust_env,
) as client:
return client.request(
method=method,
Expand All @@ -108,13 +118,14 @@ def stream(
headers: HeaderTypes = None,
cookies: CookieTypes = None,
auth: AuthTypes = None,
retries: RetriesTypes = DEFAULT_RETRIES_CONFIG,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
allow_redirects: bool = True,
verify: VerifyTypes = True,
cert: CertTypes = None,
trust_env: bool = True,
) -> StreamContextManager:
client = Client(cert=cert, verify=verify, trust_env=trust_env)
client = Client(cert=cert, verify=verify, retries=retries, trust_env=trust_env)
request = Request(
method=method,
url=url,
Expand Down Expand Up @@ -145,6 +156,7 @@ def get(
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
retries: RetriesTypes = DEFAULT_RETRIES_CONFIG,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
Expand All @@ -166,6 +178,7 @@ def get(
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
retries=retries,
timeout=timeout,
trust_env=trust_env,
)
Expand All @@ -181,6 +194,7 @@ def options(
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
retries: RetriesTypes = DEFAULT_RETRIES_CONFIG,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
Expand All @@ -202,6 +216,7 @@ def options(
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
retries=retries,
timeout=timeout,
trust_env=trust_env,
)
Expand All @@ -217,6 +232,7 @@ def head(
allow_redirects: bool = False, # Note: Differs to usual default.
cert: CertTypes = None,
verify: VerifyTypes = True,
retries: RetriesTypes = DEFAULT_RETRIES_CONFIG,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
Expand All @@ -240,6 +256,7 @@ def head(
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
retries=retries,
timeout=timeout,
trust_env=trust_env,
)
Expand All @@ -258,6 +275,7 @@ def post(
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
retries: RetriesTypes = DEFAULT_RETRIES_CONFIG,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
Expand All @@ -279,6 +297,7 @@ def post(
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
retries=retries,
timeout=timeout,
trust_env=trust_env,
)
Expand All @@ -297,6 +316,7 @@ def put(
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
retries: RetriesTypes = DEFAULT_RETRIES_CONFIG,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
Expand All @@ -318,6 +338,7 @@ def put(
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
retries=retries,
timeout=timeout,
trust_env=trust_env,
)
Expand All @@ -336,6 +357,7 @@ def patch(
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
retries: RetriesTypes = DEFAULT_RETRIES_CONFIG,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
Expand All @@ -357,6 +379,7 @@ def patch(
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
retries=retries,
timeout=timeout,
trust_env=trust_env,
)
Expand All @@ -372,6 +395,7 @@ def delete(
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
retries: RetriesTypes = DEFAULT_RETRIES_CONFIG,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
Expand All @@ -393,6 +417,7 @@ def delete(
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
retries=retries,
timeout=timeout,
trust_env=trust_env,
)
3 changes: 3 additions & 0 deletions httpx/backends/asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ async def open_uds_stream(

return SocketStream(stream_reader=stream_reader, stream_writer=stream_writer)

async def sleep(self, seconds: float) -> None:
await asyncio.sleep(seconds)

def time(self) -> float:
loop = asyncio.get_event_loop()
return loop.time()
Expand Down
3 changes: 3 additions & 0 deletions httpx/backends/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ async def open_uds_stream(
) -> BaseSocketStream:
return await self.backend.open_uds_stream(path, hostname, ssl_context, timeout)

async def sleep(self, seconds: float) -> None:
await self.backend.sleep(seconds)

def time(self) -> float:
return self.backend.time()

Expand Down
3 changes: 3 additions & 0 deletions httpx/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ async def open_uds_stream(
) -> BaseSocketStream:
raise NotImplementedError() # pragma: no cover

async def sleep(self, seconds: float) -> None:
raise NotImplementedError() # pragma: no cover

def time(self) -> float:
raise NotImplementedError() # pragma: no cover

Expand Down
6 changes: 6 additions & 0 deletions httpx/backends/sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import time


class SyncBackend:
def sleep(self, seconds: float) -> None:
time.sleep(seconds)
3 changes: 3 additions & 0 deletions httpx/backends/trio.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ async def open_uds_stream(

raise ConnectTimeout()

async def sleep(self, seconds: float) -> None:
await trio.sleep(seconds)

def time(self) -> float:
return trio.current_time()

Expand Down
Loading