2026, Jan 03 00:02

Почему requests зависает и как настроить тайм‑аут подключения и чтения в Python

Разбираем, почему запрос в Python requests замирает на чтении сокета и как предотвратить это задав timeout кортежем (подключение, чтение). Пример кода и советы.

Когда рабочий скрипт, годами выполнявшийся без проблем, внезапно замирает на сетевом вызове, ожидаешь, что тайм‑аут подстрахует остальную часть программы. Нередко же оказывается, что тайм‑аут был задан, но вызов всё равно заблокировался и функция так и не вернулась. Ниже — случай, где запрос упёрся в тайм‑аут на этапе чтения из сокета и почему более явная настройка тайм‑аутов предотвращает такие зависания.

Сценарий: тайм‑аут запроса, который не сработал

Функция вызывается много раз в день и должна быстро завершаться при сбое. В коде задан timeout в requests, вызов обёрнут в try/except, при ошибке возвращается запасное значение. Однако выполнение застыло во время обращения к API, а traceback закончился сообщением “TimeoutError: The read operation timed out”.

Проблемный пример

import requests
import json
def invoke_api():
    payload_args = {
        "someparameterhere": "example",
        "moreparameterhere": "example"
    }
    headers_map = {
        "Content-type": "application/json",
        "X-ClientLocalIP": "11.161.0.23",
        "X-ClientPublicIP": "11.22.55.66",
        "X-MACAddress": "32:01:0a:a0:00:16",
        "Accept": "application/json",
        "X-PrivateKey": "privatekeyhere",
        "X-UserType": "USER",
        "X-SourceID": "WEB",
        "Authorization": "Bearer somelongstringhere."
    }
    for _ in range(1):
        try:
            resp_obj = requests.request(
                "POST",
                "https://apiurl.com/apidataurlexample",
                data=json.dumps(payload_args),
                headers=headers_map,
                timeout=2
            ).json()
            return resp_obj
        except Exception as err:
            print(err)
    return 0

Трассировка ясно указывает: тайм‑аут произошёл на более низком уровне (ssl/socket) во время чтения, и управление не вернулось так, как ожидалось. Почему же заданный тайм‑аут не предотвратил зависание?

Что произошло на самом деле

Речь о разных «слоях» тайм‑аутов. Значение timeout, переданное в requests, обрабатывается самим стеком requests и обычно порождает requests.exceptions.Timeout. В наблюдаемом случае всплыл TimeoutError, пришедший из библиотек ниже requests на стадии чтения. Это принципиально: если тайм‑аут чтения срабатывает вне собственных исключений requests, поведение может отличаться от ожидаемого. Иначе говоря, одного тайм‑аута может не хватить, чтобы ограничить и установление соединения, и чтение ответа.

Requests поддерживает задание тайм‑аута кортежем, разделяя тайм‑аут подключения и тайм‑аут чтения. Это гарантирует, что клиент сдаётся как при установлении TCP/SSL‑соединения, так и при ожидании данных от сервера.

:param timeout: (необязательно) Сколько секунд ждать, пока сервер начнёт отправлять данные, прежде чем сдаться, — число с плавающей точкой, либо кортеж (тайм‑аут подключения, тайм‑аут чтения).

Решение: явно задать тайм‑ауты подключения и чтения

Укажите тайм‑аут как кортеж из двух значений, чтобы охватить обе стадии. Это соответствует наблюдаемому traceback с “read operation timed out” и не даст вызову «зависнуть» дольше заданного порога.

import requests
import json
def invoke_api():
    payload_args = {
        "someparameterhere": "example",
        "moreparameterhere": "example"
    }
    headers_map = {
        "Content-type": "application/json",
        "X-ClientLocalIP": "11.161.0.23",
        "X-ClientPublicIP": "11.22.55.66",
        "X-MACAddress": "32:01:0a:a0:00:16",
        "Accept": "application/json",
        "X-PrivateKey": "privatekeyhere",
        "X-UserType": "USER",
        "X-SourceID": "WEB",
        "Authorization": "Bearer somelongstringhere."
    }
    for _ in range(1):
        try:
            resp_obj = requests.request(
                "POST",
                "https://apiurl.com/apidataurlexample",
                data=json.dumps(payload_args),
                headers=headers_map,
                timeout=(2, 2)
            ).json()
            return resp_obj
        except Exception as err:
            print(err)
    return 0

Указав timeout=(2, 2), вы задаёте 2 секунды на подключение и 2 секунды на чтение. Если удалённая сторона перестанет слать данные или «застынет» после установления соединения, истечёт тайм‑аут чтения, и управление вернётся в ваш код вместо зависания.

Почему это важно

В продакшене различие между этапами подключения и чтения — не академическая тонкость. Под нагрузкой или при проблемах у upstream‑API чтение может блокироваться, даже если соединение установилось быстро. Явно задавая оба тайм‑аута, вы делаете отказ предсказуемым и не даёте функции блокировать выполнение — будь то одиночный вызов или работа из нескольких потоков.

Вывод

Если запрос «замирает» несмотря на тайм‑аут, проверьте, что вы ограничиваете и подключение, и чтение. Передавайте кортеж в timeout, чтобы функция быстро завершалась и возвращала управление, даже если чтение зависает глубоко в сетевом стеке. С такой настройкой исключения выбрасываются и перехватываются как задумано, а остальная программа продолжает работать.