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:
parent
889f914edb
commit
ed0425c60a
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue