Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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