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

ReadError("Server disconnected while attempting read") on 0.13.4 #357

Closed
jarojasm95 opened this issue Jun 9, 2021 · 7 comments · Fixed by #358
Closed

ReadError("Server disconnected while attempting read") on 0.13.4 #357

jarojasm95 opened this issue Jun 9, 2021 · 7 comments · Fixed by #358

Comments

@jarojasm95
Copy link

jarojasm95 commented Jun 9, 2021

After upgrading to 0.13.4 I am consistently getting ReadError("Server disconnected while attempting read") on all requests with no application changes.

Maybe useful info is that I am setting timeouts to:

{
connect: None, 
pool: None, 
read: None, 
write: None
}

NOTE: I am using this lib as part of using httpx

@jarojasm95
Copy link
Author

I can see this is happening while attempting to read the stream, however the anyio impl is raising EndOfStream

@tomchristie
Copy link
Member

Oh fun. Could you share a traceback? Is this happening with any request from httpx after this change or if not can you share a specific reproducible example?

@jarojasm95
Copy link
Author

Hopefully this helps:

ReadError: Server disconnected while attempting read
  File "httpx/_transports/default.py", line 61, in map_httpcore_exceptions
    yield
  File "httpx/_transports/default.py", line 201, in __aiter__
    async for part in self._httpcore_stream:
  File "httpcore/_async/connection_pool.py", line 57, in __aiter__
    async for chunk in self.stream:
  File "httpcore/_bytestreams.py", line 91, in __aiter__
    async for chunk in self._aiterator:
  File "httpcore/_async/http11.py", line 208, in _receive_response_data
    event = await self._receive_event(timeout)
  File "httpcore/_async/http11.py", line 225, in _receive_event
    data = await self.socket.read(self.READ_NUM_BYTES, timeout)
  File "httpcore/_backends/anyio.py", line 65, in read
    raise ReadError("Server disconnected while attempting read") from None
ReadError: Server disconnected while attempting read
  File "tornado/web.py", line 1704, in _execute
    result = await result
  File "/home/app/handlers/base.py", line 33, in handler
    bundle = await func(self, *args, **kwargs)
  File "/home/app/handlers/business/__init__.py", line 189, in get
    business: Business = await Business.get(software_id)
  File "/home/app/libs/core.py", line 154, in wrapper
    v = await method(self, *args, **kwargs)
  File "/home/app/models/business.py", line 89, in get
    response = await Api().get("xxxxxxxxxxx")
  File "httpx/_client.py", line 1661, in get
    return await self.request(
  File "httpx/_client.py", line 1425, in request
    response = await self.send(
  File "httpx/_client.py", line 1528, in send
    raise exc
  File "httpx/_client.py", line 1519, in send
    await response.aread()
  File "httpx/_models.py", line 1561, in aread
    self._content = b"".join([part async for part in self.aiter_bytes()])
  File "httpx/_models.py", line 1561, in <listcomp>
    self._content = b"".join([part async for part in self.aiter_bytes()])
  File "httpx/_models.py", line 1577, in aiter_bytes
    async for raw_bytes in self.aiter_raw():
  File "httpx/_models.py", line 1631, in aiter_raw
    async for raw_stream_bytes in self.stream:
  File "httpx/_client.py", line 127, in __aiter__
    async for chunk in self._stream:
  File "httpx/_transports/default.py", line 202, in __aiter__
    yield part
  File "contextlib.py", line 131, in __exit__
    self.gen.throw(type, value, traceback)
  File "httpx/_transports/default.py", line 78, in map_httpcore_exceptions
    raise mapped_exc(message) from exc

@jarojasm95
Copy link
Author

From what I could tell it was happening on all requests, maybe helpful detail - these were non-SSL requests ("http://"). I can try and put together a reproducible example later today or tomorrow

@tomchristie
Copy link
Member

Right, well I wasn't able to reproduce this, so at least it's not as impactful as I'd first assumed, but I can see where the issue is coming from.

Our AnyIO backend is handling EOF-on-read differently from the other backends...

Here...

except EndOfStream:
raise ReadError("Server disconnected while attempting read") from None

We're raising ReadError("Server disconnected while attempting read") rather than returning the sentinal "" that other backends use for signalling EOF.

By contrast, here's Trio...

return await self.stream.receive_some(max_bytes=n)

Our previous native asyncio case...

return await asyncio.wait_for(
self.stream_reader.read(n), timeout.get("read")
)

And the sync case...

return self.sock.recv(n)

Switching to anyio was a big codechange, so it was always going to have some associated risk

Trio's receive_some method is probably the best point of comparison, since they're (as always) so extra careful about documenting the fiddly bits.

A return value of b"" (an empty bytestring) indicates that the stream has reached end-of-file. Implementations should be careful that they return b"" if, and only if, the stream has reached end-of-file

In contrast, anyio's receive() method uses an EndOfStream exception on EOF. Instead of mapping that case onto a ReadError we should simply return "" since that's the EOF contract our HTTP handling code is expecting.

It would probably be worth getting some docs tweaking from @agronholm around the expectations on the receive() method, because it is currently ambiguous if the method can return an empty bytestring or not.

Iterating this byte stream will yield a byte string of arbitrary length, but no more than 65536 bytes.

It'd be somewhat awkward if it can, because we'd need to special case that, but I'd assume it can't (shouldn't ever) and that there's just a point of documentation that needs explicitly firming up.

("wait til you've got some data and then give it to me... okay here's something-or-possibly-actually-nothing for you" would be a weird contract to have.)

@jarojasm95
Copy link
Author

thanks for taking a look so promptly! I appreciate it, a small piece of feedback would be that maybe a change like the introduction of anyio as a backend was a bit too large of a change for a minor version bump.

@tomchristie
Copy link
Member

tomchristie commented Jun 14, 2021

@jarojasm95 Possibly so, yes. There wasn't any API requirements for it to be a median release bump, but it clearly had a risk factor attached, which a median release would have signalled more clearly. (And some smart folks might be pinning against the median version.)

Anyways, version 0.13.5 has now been released.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants