httpx: How to make a connection never timeout

Problem

I use httpx to request a kubernetes watch api. The api is based on HTTP chunked transfer encoding. When the server generates a new pod message, it will send to client.

Simplified code is as follows

import httpx

resp = httpx.get('http://127.0.0.1:18081/api/v1/watch/pods', stream=True)
for chunk in resp.stream():
    pass

After 5 second, I get a ReadTimeout error

Traceback (most recent call last):
  File "t.py", line 4, in <module>
    for line in resp.stream():
  File "/usr/local/lib/python3.6/site-packages/httpx/models.py", line 1077, in stream
    for chunk in self.raw():
  File "/usr/local/lib/python3.6/site-packages/httpx/models.py", line 1105, in raw
    for part in self._raw_stream:
  File "/usr/local/lib/python3.6/site-packages/httpx/concurrency/base.py", line 163, in iterate
    yield self.run(async_iterator.__anext__)
  File "/usr/local/lib/python3.6/site-packages/httpx/concurrency/asyncio.py", line 261, in run
    return self.loop.run_until_complete(coroutine(*args, **kwargs))
  File "/usr/lib64/python3.6/asyncio/base_events.py", line 484, in run_until_complete
    return future.result()
  File "/usr/local/lib/python3.6/site-packages/httpx/dispatch/http11.py", line 152, in _receive_response_data
    event = await self._receive_event(timeout)
  File "/usr/local/lib/python3.6/site-packages/httpx/dispatch/http11.py", line 174, in _receive_event
    self.READ_NUM_BYTES, timeout, flag=self.timeout_flag
  File "/usr/local/lib/python3.6/site-packages/httpx/concurrency/asyncio.py", line 83, in read
    raise ReadTimeout() from None
httpx.exceptions.ReadTimeout

I change the timeout to 60 sec, and I will get RemoteProtocolError

Traceback (most recent call last):
  File "t.py", line 4, in <module>
    for chunk in resp.stream():
  File "/usr/local/lib/python3.6/site-packages/httpx/models.py", line 1077, in stream
    for chunk in self.raw():
  File "/usr/local/lib/python3.6/site-packages/httpx/models.py", line 1105, in raw
    for part in self._raw_stream:
  File "/usr/local/lib/python3.6/site-packages/httpx/concurrency/base.py", line 163, in iterate
    yield self.run(async_iterator.__anext__)
  File "/usr/local/lib/python3.6/site-packages/httpx/concurrency/asyncio.py", line 261, in run
    return self.loop.run_until_complete(coroutine(*args, **kwargs))
  File "/usr/lib64/python3.6/asyncio/base_events.py", line 484, in run_until_complete
    return future.result()
  File "/usr/local/lib/python3.6/site-packages/httpx/dispatch/http11.py", line 152, in _receive_response_data
    event = await self._receive_event(timeout)
  File "/usr/local/lib/python3.6/site-packages/httpx/dispatch/http11.py", line 164, in _receive_event
    event = self.h11_state.next_event()
  File "/usr/local/lib/python3.6/site-packages/h11/_connection.py", line 420, in next_event
    event = self._extract_next_receive_event()
  File "/usr/local/lib/python3.6/site-packages/h11/_connection.py", line 369, in _extract_next_receive_event
    event = self._reader.read_eof()
  File "/usr/local/lib/python3.6/site-packages/h11/_readers.py", line 170, in read_eof
    "peer closed connection without sending complete message body "
h11._util.RemoteProtocolError: peer closed connection without sending complete message body (incomplete chunked read)

Environment

  • Python 3.6.3
  • httpx 0.7.4

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 1
  • Comments: 19 (17 by maintainers)

Most upvoted comments

Note, TimeoutConfig is now simply called Timeout.

There are actually three kinds of timeouts you can control in HTTPX: connect, read, and write. Unfortunately, the documentation doesn’t show this ability to fine-tune timeouts yet (TimeoutConfig isn’t even documented), and I agree it’s something we need to address.

For your use case, it seems what you want is to modify the read timeout. (That way, you’ll still timeout when connecting or when sending data, which is good because those are unexpected situations.)

The following should work:

import httpx

timeout = httpx.TimeoutConfig(connect_timeout=5, read_timeout=None, write_timeout=5)

resp = httpx.get('http://127.0.0.1:18081/api/v1/watch/pods', stream=True, timeout=timeout)
for chunk in resp.stream():
    pass

Unfortunately there’s no way to override only one of the timeouts while keeping default values for the others, so you need to explicitly set the connect and write timeouts. I reckon what we’d need is something like:

timeout = httpx.DEFAULT_TIMEOUT_CONFIG.replace(read_timeout=None)

but that doesn’t exist yet.

Also — according to this SO thread you should expect the connection to drop anyway, so a long timeout along with an infinite loop that reconnects when losing the connection to the watch endpoint might be preferable:

def get_pod_events():
    timeout = httpx.TimeoutConfig(connect_timeout=5, read_timeout=5 * 60 write_timeout=5)
    while True:
        resp = httpx.get('http://127.0.0.1:18081/api/v1/watch/pods', stream=True, timeout=timeout)
        for event in resp.stream():
            yield event

Ah, this seems like it might be a bug. Setting timeout=None on a client instance works properly. Setting timeout=None on any of the request calls, regardless if its through the top-level API or through a client method uses the default timeout.

Edit: That is, if you used:

import httpx

client = httpx.Client(timeout=None)
resp = client.get('http://127.0.0.1:18081/api/v1/watch/pods', stream=True)
for chunk in resp.stream():
    pass

you should get the behavior you want for now.

I definitely prefer the first option. timeout=False doesn’t really make sense when read. And timeout=None is how it’s spelled in requests.

This is a difference coming from requests I ran into as well. httpx sets a default timeout, so you’ll have to add timeout=None to your request calls or client instance (if used) if you want no timeout.

It would probably be nice to add to the Timeouts docs as well as the requests compatibility docs when they get fleshed out. I have a feeling this one will come up a fair bit.