Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions cheroot/makefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ def write(self, b):
def _flush_unlocked(self):
self._checkClosed('flush of closed file')
while self._write_buf:
n = None
try:
# ssl sockets only except 'bytes', not bytearrays
# so perhaps we should conditionally wrap this for perf?
n = self.raw.write(bytes(self._write_buf))
n = self.raw.write(
bytes(self._write_buf[:SOCK_WRITE_BLOCKSIZE]),
)
except io.BlockingIOError as e:
n = e.characters_written
del self._write_buf[:n]
if n:
del self._write_buf[:n]


class StreamReader(io.BufferedReader):
Expand Down
43 changes: 43 additions & 0 deletions cheroot/test/test_makefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,46 @@ def test_bytes_written():
wfile = makefile.MakeFile(sock, 'w')
wfile.write(b'bar')
assert wfile.bytes_written == 3


class _RawWriteBlockOnce:
"""Mock raw.write() that returns None on the first call, then writes normally."""

def __init__(self):
"""Initialize _RawWriteBlockOnce."""
self.call_count = 0
self.written = bytearray()

def __call__(self, chunk):
"""Return None on first call to simulate a blocked socket write."""
self.call_count += 1
if self.call_count == 1:
return (
None # simulates socket returning None on first blocked write
)
self.written.extend(chunk)
return len(chunk)


def test_flush_when_raw_write_returns_none():
"""_flush_unlocked() must not treat None from raw.write() as a byte count.

io.RawIOBase.write() returns None when a non-blocking socket cannot accept
data. del self._write_buf[:None] is equivalent to del self._write_buf[:]
which silently clears the entire buffer, truncating the response without
raising an exception.
"""
data = b'x' * (makefile.SOCK_WRITE_BLOCKSIZE * 2) # stress the write loop

sock = MockSocket()
wfile = makefile.MakeFile(sock, 'w')
wfile._write_buf.extend(data)

mock = _RawWriteBlockOnce()
wfile.raw.write = mock
wfile._flush_unlocked()

assert bytes(mock.written) == data, (
f'Expected {len(data)} bytes but only {len(mock.written)} reached raw.write(): '
'buffer was silently discarded when raw.write() returned None'
)
3 changes: 3 additions & 0 deletions docs/changelog-fragments.d/822.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fixed a bug that could cause premature clearing of the write buffer when a socket write is blocked.

-- by :user:`cbbm142`
3 changes: 3 additions & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ backports
bugfixes
builtin
b'xb
buf
compat
config
conftest
Expand All @@ -22,6 +23,7 @@ hardcoded
hostname
inclusivity
intersphinx
io
iterable
linter
linters
Expand All @@ -48,6 +50,7 @@ preconfigure
py
pytest
pythonic
RawIOBase
readonly
rebase
Refactor
Expand Down
Loading