Skip to content

Commit 34eeed4

Browse files
committed
Issue #26721: Change StreamRequestHandler.wfile to BufferedIOBase
1 parent 7acc348 commit 34eeed4

File tree

7 files changed

+131
-15
lines changed

7 files changed

+131
-15
lines changed

Doc/library/http.server.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,18 @@ of which this module provides three different variants:
9898

9999
.. attribute:: rfile
100100

101-
Contains an input stream, positioned at the start of the optional input
102-
data.
101+
An :class:`io.BufferedIOBase` input stream, ready to read from
102+
the start of the optional input data.
103103

104104
.. attribute:: wfile
105105

106106
Contains the output stream for writing a response back to the
107107
client. Proper adherence to the HTTP protocol must be used when writing to
108108
this stream.
109109

110+
.. versionchanged:: 3.6
111+
This is an :class:`io.BufferedIOBase` stream.
112+
110113
:class:`BaseHTTPRequestHandler` has the following attributes:
111114

112115
.. attribute:: server_version

Doc/library/socketserver.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,15 @@ Request Handler Objects
409409
read or written, respectively, to get the request data or return data
410410
to the client.
411411

412+
The :attr:`rfile` attributes of both classes support the
413+
:class:`io.BufferedIOBase` readable interface, and
414+
:attr:`DatagramRequestHandler.wfile` supports the
415+
:class:`io.BufferedIOBase` writable interface.
416+
417+
.. versionchanged:: 3.6
418+
:attr:`StreamRequestHandler.wfile` also supports the
419+
:class:`io.BufferedIOBase` writable interface.
420+
412421

413422
Examples
414423
--------

Doc/whatsnew/3.6.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,12 @@ defined in :mod:`http.server`, :mod:`xmlrpc.server` and
373373
protocol.
374374
(Contributed by Aviv Palivoda in :issue:`26404`.)
375375

376+
The :attr:`~socketserver.StreamRequestHandler.wfile` attribute of
377+
:class:`~socketserver.StreamRequestHandler` classes now implements
378+
the :class:`io.BufferedIOBase` writable interface. In particular,
379+
calling :meth:`~io.BufferedIOBase.write` is now guaranteed to send the
380+
data in full. (Contributed by Martin Panter in :issue:`26721`.)
381+
376382

377383
subprocess
378384
----------

Lib/socketserver.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ class will essentially render the service "deaf" while one request is
132132
import threading
133133
except ImportError:
134134
import dummy_threading as threading
135+
from io import BufferedIOBase
135136
from time import monotonic as time
136137

137138
__all__ = ["BaseServer", "TCPServer", "UDPServer",
@@ -743,7 +744,10 @@ def setup(self):
743744
self.connection.setsockopt(socket.IPPROTO_TCP,
744745
socket.TCP_NODELAY, True)
745746
self.rfile = self.connection.makefile('rb', self.rbufsize)
746-
self.wfile = self.connection.makefile('wb', self.wbufsize)
747+
if self.wbufsize == 0:
748+
self.wfile = _SocketWriter(self.connection)
749+
else:
750+
self.wfile = self.connection.makefile('wb', self.wbufsize)
747751

748752
def finish(self):
749753
if not self.wfile.closed:
@@ -756,6 +760,24 @@ def finish(self):
756760
self.wfile.close()
757761
self.rfile.close()
758762

763+
class _SocketWriter(BufferedIOBase):
764+
"""Simple writable BufferedIOBase implementation for a socket
765+
766+
Does not hold data in a buffer, avoiding any need to call flush()."""
767+
768+
def __init__(self, sock):
769+
self._sock = sock
770+
771+
def writable(self):
772+
return True
773+
774+
def write(self, b):
775+
self._sock.sendall(b)
776+
with memoryview(b) as view:
777+
return view.nbytes
778+
779+
def fileno(self):
780+
return self._sock.fileno()
759781

760782
class DatagramRequestHandler(BaseRequestHandler):
761783

Lib/test/test_socketserver.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import contextlib
6+
import io
67
import os
78
import select
89
import signal
@@ -376,6 +377,84 @@ def wait_done(self):
376377
self.active_children.clear()
377378

378379

380+
class SocketWriterTest(unittest.TestCase):
381+
def test_basics(self):
382+
class Handler(socketserver.StreamRequestHandler):
383+
def handle(self):
384+
self.server.wfile = self.wfile
385+
self.server.wfile_fileno = self.wfile.fileno()
386+
self.server.request_fileno = self.request.fileno()
387+
388+
server = socketserver.TCPServer((HOST, 0), Handler)
389+
self.addCleanup(server.server_close)
390+
s = socket.socket(
391+
server.address_family, socket.SOCK_STREAM, socket.IPPROTO_TCP)
392+
with s:
393+
s.connect(server.server_address)
394+
server.handle_request()
395+
self.assertIsInstance(server.wfile, io.BufferedIOBase)
396+
self.assertEqual(server.wfile_fileno, server.request_fileno)
397+
398+
@unittest.skipUnless(threading, 'Threading required for this test.')
399+
def test_write(self):
400+
# Test that wfile.write() sends data immediately, and that it does
401+
# not truncate sends when interrupted by a Unix signal
402+
pthread_kill = test.support.get_attribute(signal, 'pthread_kill')
403+
404+
class Handler(socketserver.StreamRequestHandler):
405+
def handle(self):
406+
self.server.sent1 = self.wfile.write(b'write data\n')
407+
# Should be sent immediately, without requiring flush()
408+
self.server.received = self.rfile.readline()
409+
big_chunk = bytes(test.support.SOCK_MAX_SIZE)
410+
self.server.sent2 = self.wfile.write(big_chunk)
411+
412+
server = socketserver.TCPServer((HOST, 0), Handler)
413+
self.addCleanup(server.server_close)
414+
interrupted = threading.Event()
415+
416+
def signal_handler(signum, frame):
417+
interrupted.set()
418+
419+
original = signal.signal(signal.SIGUSR1, signal_handler)
420+
self.addCleanup(signal.signal, signal.SIGUSR1, original)
421+
response1 = None
422+
received2 = None
423+
main_thread = threading.get_ident()
424+
425+
def run_client():
426+
s = socket.socket(server.address_family, socket.SOCK_STREAM,
427+
socket.IPPROTO_TCP)
428+
with s, s.makefile('rb') as reader:
429+
s.connect(server.server_address)
430+
nonlocal response1
431+
response1 = reader.readline()
432+
s.sendall(b'client response\n')
433+
434+
reader.read(100)
435+
# The main thread should now be blocking in a send() syscall.
436+
# But in theory, it could get interrupted by other signals,
437+
# and then retried. So keep sending the signal in a loop, in
438+
# case an earlier signal happens to be delivered at an
439+
# inconvenient moment.
440+
while True:
441+
pthread_kill(main_thread, signal.SIGUSR1)
442+
if interrupted.wait(timeout=float(1)):
443+
break
444+
nonlocal received2
445+
received2 = len(reader.read())
446+
447+
background = threading.Thread(target=run_client)
448+
background.start()
449+
server.handle_request()
450+
background.join()
451+
self.assertEqual(server.sent1, len(response1))
452+
self.assertEqual(response1, b'write data\n')
453+
self.assertEqual(server.received, b'client response\n')
454+
self.assertEqual(server.sent2, test.support.SOCK_MAX_SIZE)
455+
self.assertEqual(received2, test.support.SOCK_MAX_SIZE - 100)
456+
457+
379458
class MiscTestCase(unittest.TestCase):
380459

381460
def test_all(self):

Lib/wsgiref/simple_server.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"""
1212

1313
from http.server import BaseHTTPRequestHandler, HTTPServer
14-
from io import BufferedWriter
1514
import sys
1615
import urllib.parse
1716
from wsgiref.handlers import SimpleHandler
@@ -127,17 +126,11 @@ def handle(self):
127126
if not self.parse_request(): # An error code has been sent, just exit
128127
return
129128

130-
# Avoid passing the raw file object wfile, which can do partial
131-
# writes (Issue 24291)
132-
stdout = BufferedWriter(self.wfile)
133-
try:
134-
handler = ServerHandler(
135-
self.rfile, stdout, self.get_stderr(), self.get_environ()
136-
)
137-
handler.request_handler = self # backpointer for logging
138-
handler.run(self.server.get_app())
139-
finally:
140-
stdout.detach()
129+
handler = ServerHandler(
130+
self.rfile, self.wfile, self.get_stderr(), self.get_environ()
131+
)
132+
handler.request_handler = self # backpointer for logging
133+
handler.run(self.server.get_app())
141134

142135

143136

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ What's New in Python 3.6.0 alpha 3
1010
Library
1111
-------
1212

13+
- Issue #26721: Change the socketserver.StreamRequestHandler.wfile attribute
14+
to implement BufferedIOBase. In particular, the write() method no longer
15+
does partial writes.
16+
1317
- Issue #22115: Added methods trace_add, trace_remove and trace_info in the
1418
tkinter.Variable class. They replace old methods trace_variable, trace,
1519
trace_vdelete and trace_vinfo that use obsolete Tcl commands and might

0 commit comments

Comments
 (0)