How to Patch Requests to Have a Default Timeout

Summon forth the armies, but with a timeout of five seconds.

Python’s requests package is very popular. Even if you don’t use it directly, it’s highly likely one of your dependencies does.

One wrinkle in requests’ design is that it has no default timeout. This means that requests can hang forever if the remote server doesn’t respond, unless the author remembered to add a timeout. Issue #3070 tracks the discussion on adding such a default timeout, but it has been open several years. httpx learned a lesson from this and it has default timeout of five seconds.

This “missing default” has caused several production incidents at my client ev.energy. Remote server outages caused background tasks to take 45 minutes instead of 4.5 seconds, waiting for responses that wouldn’t come. This caused other important background work to be delayed, with knock-on effects.

Auditing the codebase to add missing timeout parameters only got so far. New third party and first party code “slipped through the net” and was deployed without a timeout.

I came up with a solution to change requests to use a default timeout. I did so with patchy, my package for patching the source code of functions at runtime. Patchy provides an alternative to monkey-patching with a few advantages: it can modify lines in the body of a function, it fails if the target function changes, and references to the function don’t need updating.

Below is the patching function. Feel free to copy it into your project!

import patchy
from requests.adapters import HTTPAdapter


def patch_requests_default_timeout() -> None:
    """
    Set a default timeout for all requests made with “requests”.

    Upstream is waiting on this longstanding issue:
    https://github.com/psf/requests/issues/3070
    """

    patchy.patch(
        HTTPAdapter.send,
        """\
        @@ -14,6 +14,8 @@
             :param proxies: (optional) The proxies dictionary to apply to the request.
             :rtype: requests.Response
             \"""
        +    if timeout is None:
        +        timeout = 5.0

             try:
                 conn = self.get_connection(request.url, proxies)
        """,
    )

You need to call this once when your project initializes. On a Django project, this can be done in an AppConfig:

from django.apps import AppConfig

from example.core import backports


class ExampleConfig(AppConfig):
    name = "example"

    def ready(self) -> None:
        backports.requests.patch_default_timeout()

The above snippet uses a 5 second timeout, which copies from httpx. This may be a bit low for existing projects integrated with several services. You may wish to start higher and iterate down.

At ev.energy, I used a higher value of 30 seconds, since production metrics showed some third party services taking a while to respond. I’m planning on iteratively reducing the default timeout down to 5 seconds, whilst setting explicit longer timeouts for the few known-slow services.

Fin

—Adam


Improve your Django develompent experience with my new book.


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: ,