From 758d609c64404f2b77e2f62e5b33b5cbe55ba0cb Mon Sep 17 00:00:00 2001 From: afonsojanu Date: Mon, 7 Sep 2026 10:45:23 +0100 Subject: [PATCH] Handle read() returning None on a non-blocking socket Under a non-blocking socket (cheroot supports this via timeout=0), io.BufferedReader.read() can legitimately return None instead of b'' when there's no data available yet - that's documented behavior in _pyio's raw IO contract, not an error case. StreamReader.read() didn't account for this and crashed with TypeError: object of type 'NoneType' has no len() the moment a non-blocking read came back empty, which in practice meant it could blow up mid-request whenever the client hadn't sent anything yet. Fixes cherrypy/cheroot#278. --- cheroot/makefile.py | 6 +++++- cheroot/test/test_makefile.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cheroot/makefile.py b/cheroot/makefile.py index f5780a1ede..9df2fe2710 100644 --- a/cheroot/makefile.py +++ b/cheroot/makefile.py @@ -46,7 +46,11 @@ def __init__(self, sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE): def read(self, *args, **kwargs): """Capture bytes read.""" val = super().read(*args, **kwargs) - self.bytes_read += len(val) + # A non-blocking socket with nothing available yet can make the + # underlying BufferedReader return None instead of b''. That's + # documented io.RawIOBase behavior, so don't choke on it here. + if val is not None: + self.bytes_read += len(val) return val def has_data(self): diff --git a/cheroot/test/test_makefile.py b/cheroot/test/test_makefile.py index d65d4ea268..5fd5f20de7 100644 --- a/cheroot/test/test_makefile.py +++ b/cheroot/test/test_makefile.py @@ -1,5 +1,7 @@ """Tests for :py:mod:`cheroot.makefile`.""" +import errno + from cheroot import makefile @@ -9,9 +11,12 @@ class MockSocket: def __init__(self): """Initialize :py:class:`MockSocket`.""" self.messages = [] + self.would_block = False def recv_into(self, buf): """Simulate ``recv_into`` for Python 3.""" + if self.would_block: + raise BlockingIOError(errno.EWOULDBLOCK, 'would block') if not self.messages: return 0 msg = self.messages.pop(0) @@ -44,6 +49,20 @@ def test_bytes_read(): assert rfile.bytes_read == 3 +def test_read_on_nonblocking_socket_with_no_data(): + """Reader should return ``None``, not crash, if nothing is available yet. + + A non-blocking socket's ``read()`` can return ``None`` per the + documented ``io.RawIOBase`` contract when no data is available; this + used to blow up on ``len(None)``. Ref: cherrypy/cheroot#278 + """ + sock = MockSocket() + sock.would_block = True + rfile = makefile.MakeFile(sock, 'r') + assert rfile.read(256) is None + assert rfile.bytes_read == 0 + + def test_bytes_written(): """Writer should capture bytes written.""" sock = MockSocket()