Issue #24291: Avoid WSGIRequestHandler doing partial writes

If the underlying send() method indicates a partial write, such as when the
call is interrupted to handle a signal, the server would silently drop the
remaining data.

Also add deprecated support for SimpleHandler.stdout.write() doing partial
writes.
This commit is contained in:
Martin Panter 2016-06-05 06:28:55 +00:00
parent 889f914edb
commit ed0425c60a
5 changed files with 111 additions and 7 deletions

View File

@ -515,6 +515,9 @@ input, output, and error streams.
streams are stored in the :attr:`stdin`, :attr:`stdout`, :attr:`stderr`, and streams are stored in the :attr:`stdin`, :attr:`stdout`, :attr:`stderr`, and
:attr:`environ` attributes. :attr:`environ` attributes.
The :meth:`~io.BufferedIOBase.write` method of *stdout* should write
each chunk in full, like :class:`io.BufferedIOBase`.
.. class:: BaseHandler() .. class:: BaseHandler()

View File

@ -1,18 +1,22 @@
from unittest import mock from unittest import mock
from test import support
from test.test_httpservers import NoLogRequestHandler
from unittest import TestCase from unittest import TestCase
from wsgiref.util import setup_testing_defaults from wsgiref.util import setup_testing_defaults
from wsgiref.headers import Headers from wsgiref.headers import Headers
from wsgiref.handlers import BaseHandler, BaseCGIHandler from wsgiref.handlers import BaseHandler, BaseCGIHandler, SimpleHandler
from wsgiref import util from wsgiref import util
from wsgiref.validate import validator from wsgiref.validate import validator
from wsgiref.simple_server import WSGIServer, WSGIRequestHandler from wsgiref.simple_server import WSGIServer, WSGIRequestHandler
from wsgiref.simple_server import make_server from wsgiref.simple_server import make_server
from http.client import HTTPConnection
from io import StringIO, BytesIO, BufferedReader from io import StringIO, BytesIO, BufferedReader
from socketserver import BaseServer from socketserver import BaseServer
from platform import python_implementation from platform import python_implementation
import os import os
import re import re
import signal
import sys import sys
import unittest import unittest
@ -245,6 +249,56 @@ class IntegrationTests(TestCase):
], ],
out.splitlines()) out.splitlines())
def test_interrupted_write(self):
# BaseHandler._write() and _flush() have to write all data, even if
# it takes multiple send() calls. Test this by interrupting a send()
# call with a Unix signal.
threading = support.import_module("threading")
pthread_kill = support.get_attribute(signal, "pthread_kill")
def app(environ, start_response):
start_response("200 OK", [])
return [bytes(support.SOCK_MAX_SIZE)]
class WsgiHandler(NoLogRequestHandler, WSGIRequestHandler):
pass
server = make_server(support.HOST, 0, app, handler_class=WsgiHandler)
self.addCleanup(server.server_close)
interrupted = threading.Event()
def signal_handler(signum, frame):
interrupted.set()
original = signal.signal(signal.SIGUSR1, signal_handler)
self.addCleanup(signal.signal, signal.SIGUSR1, original)
received = None
main_thread = threading.get_ident()
def run_client():
http = HTTPConnection(*server.server_address)
http.request("GET", "/")
with http.getresponse() as response:
response.read(100)
# The main thread should now be blocking in a send() system
# call. But in theory, it could get interrupted by other
# signals, and then retried. So keep sending the signal in a
# loop, in case an earlier signal happens to be delivered at
# an inconvenient moment.
while True:
pthread_kill(main_thread, signal.SIGUSR1)
if interrupted.wait(timeout=float(1)):
break
nonlocal received
received = len(response.read())
http.close()
background = threading.Thread(target=run_client)
background.start()
server.handle_request()
background.join()
self.assertEqual(received, support.SOCK_MAX_SIZE - 100)
class UtilityTests(TestCase): class UtilityTests(TestCase):
@ -701,6 +755,31 @@ class HandlerTests(TestCase):
h.run(error_app) h.run(error_app)
self.assertEqual(side_effects['close_called'], True) self.assertEqual(side_effects['close_called'], True)
def testPartialWrite(self):
written = bytearray()
class PartialWriter:
def write(self, b):
partial = b[:7]
written.extend(partial)
return len(partial)
def flush(self):
pass
environ = {"SERVER_PROTOCOL": "HTTP/1.0"}
h = SimpleHandler(BytesIO(), PartialWriter(), sys.stderr, environ)
msg = "should not do partial writes"
with self.assertWarnsRegex(DeprecationWarning, msg):
h.run(hello_app)
self.assertEqual(b"HTTP/1.0 200 OK\r\n"
b"Content-Type: text/plain\r\n"
b"Date: Mon, 05 Jun 2006 18:49:54 GMT\r\n"
b"Content-Length: 13\r\n"
b"\r\n"
b"Hello, world!",
written)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -450,7 +450,17 @@ class SimpleHandler(BaseHandler):
self.environ.update(self.base_env) self.environ.update(self.base_env)
def _write(self,data): def _write(self,data):
self.stdout.write(data) result = self.stdout.write(data)
if result is None or result == len(data):
return
from warnings import warn
warn("SimpleHandler.stdout.write() should not do partial writes",
DeprecationWarning)
while True:
data = data[result:]
if not data:
break
result = self.stdout.write(data)
def _flush(self): def _flush(self):
self.stdout.flush() self.stdout.flush()

View File

@ -11,6 +11,7 @@ module. See also the BaseHTTPServer module docs for other API information.
""" """
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from io import BufferedWriter
import sys import sys
import urllib.parse import urllib.parse
from wsgiref.handlers import SimpleHandler from wsgiref.handlers import SimpleHandler
@ -126,11 +127,17 @@ class WSGIRequestHandler(BaseHTTPRequestHandler):
if not self.parse_request(): # An error code has been sent, just exit if not self.parse_request(): # An error code has been sent, just exit
return return
handler = ServerHandler( # Avoid passing the raw file object wfile, which can do partial
self.rfile, self.wfile, self.get_stderr(), self.get_environ() # writes (Issue 24291)
) stdout = BufferedWriter(self.wfile)
handler.request_handler = self # backpointer for logging try:
handler.run(self.server.get_app()) handler = ServerHandler(
self.rfile, stdout, self.get_stderr(), self.get_environ()
)
handler.request_handler = self # backpointer for logging
handler.run(self.server.get_app())
finally:
stdout.detach()

View File

@ -131,6 +131,11 @@ Core and Builtins
Library Library
------- -------
- Issue #24291: Fix wsgiref.simple_server.WSGIRequestHandler to completely
write data to the client. Previously it could do partial writes and
truncate data. Also, wsgiref.handler.ServerHandler can now handle stdout
doing partial writes, but this is deprecated.
- Issue #26809: Add ``__all__`` to :mod:`string`. Patch by Emanuel Barry. - Issue #26809: Add ``__all__`` to :mod:`string`. Patch by Emanuel Barry.
- Issue #26373: subprocess.Popen.communicate now correctly ignores - Issue #26373: subprocess.Popen.communicate now correctly ignores