Skip to content

Commit ff5cd45

Browse files
committed
Issue python#24291: Merge wsgi partial write fix from 3.5
2 parents 1b749c5 + ed0425c commit ff5cd45

5 files changed

Lines changed: 111 additions & 7 deletions

File tree

Doc/library/wsgiref.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,9 @@ input, output, and error streams.
515515
streams are stored in the :attr:`stdin`, :attr:`stdout`, :attr:`stderr`, and
516516
:attr:`environ` attributes.
517517

518+
The :meth:`~io.BufferedIOBase.write` method of *stdout* should write
519+
each chunk in full, like :class:`io.BufferedIOBase`.
520+
518521

519522
.. class:: BaseHandler()
520523

Lib/test/test_wsgiref.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
from unittest import mock
2+
from test import support
3+
from test.test_httpservers import NoLogRequestHandler
24
from unittest import TestCase
35
from wsgiref.util import setup_testing_defaults
46
from wsgiref.headers import Headers
5-
from wsgiref.handlers import BaseHandler, BaseCGIHandler
7+
from wsgiref.handlers import BaseHandler, BaseCGIHandler, SimpleHandler
68
from wsgiref import util
79
from wsgiref.validate import validator
810
from wsgiref.simple_server import WSGIServer, WSGIRequestHandler
911
from wsgiref.simple_server import make_server
12+
from http.client import HTTPConnection
1013
from io import StringIO, BytesIO, BufferedReader
1114
from socketserver import BaseServer
1215
from platform import python_implementation
1316

1417
import os
1518
import re
19+
import signal
1620
import sys
1721
import unittest
1822

@@ -245,6 +249,56 @@ def app(e, s):
245249
],
246250
out.splitlines())
247251

252+
def test_interrupted_write(self):
253+
# BaseHandler._write() and _flush() have to write all data, even if
254+
# it takes multiple send() calls. Test this by interrupting a send()
255+
# call with a Unix signal.
256+
threading = support.import_module("threading")
257+
pthread_kill = support.get_attribute(signal, "pthread_kill")
258+
259+
def app(environ, start_response):
260+
start_response("200 OK", [])
261+
return [bytes(support.SOCK_MAX_SIZE)]
262+
263+
class WsgiHandler(NoLogRequestHandler, WSGIRequestHandler):
264+
pass
265+
266+
server = make_server(support.HOST, 0, app, handler_class=WsgiHandler)
267+
self.addCleanup(server.server_close)
268+
interrupted = threading.Event()
269+
270+
def signal_handler(signum, frame):
271+
interrupted.set()
272+
273+
original = signal.signal(signal.SIGUSR1, signal_handler)
274+
self.addCleanup(signal.signal, signal.SIGUSR1, original)
275+
received = None
276+
main_thread = threading.get_ident()
277+
278+
def run_client():
279+
http = HTTPConnection(*server.server_address)
280+
http.request("GET", "/")
281+
with http.getresponse() as response:
282+
response.read(100)
283+
# The main thread should now be blocking in a send() system
284+
# call. But in theory, it could get interrupted by other
285+
# signals, and then retried. So keep sending the signal in a
286+
# loop, in case an earlier signal happens to be delivered at
287+
# an inconvenient moment.
288+
while True:
289+
pthread_kill(main_thread, signal.SIGUSR1)
290+
if interrupted.wait(timeout=float(1)):
291+
break
292+
nonlocal received
293+
received = len(response.read())
294+
http.close()
295+
296+
background = threading.Thread(target=run_client)
297+
background.start()
298+
server.handle_request()
299+
background.join()
300+
self.assertEqual(received, support.SOCK_MAX_SIZE - 100)
301+
248302

249303
class UtilityTests(TestCase):
250304

@@ -701,6 +755,31 @@ def close(self):
701755
h.run(error_app)
702756
self.assertEqual(side_effects['close_called'], True)
703757

758+
def testPartialWrite(self):
759+
written = bytearray()
760+
761+
class PartialWriter:
762+
def write(self, b):
763+
partial = b[:7]
764+
written.extend(partial)
765+
return len(partial)
766+
767+
def flush(self):
768+
pass
769+
770+
environ = {"SERVER_PROTOCOL": "HTTP/1.0"}
771+
h = SimpleHandler(BytesIO(), PartialWriter(), sys.stderr, environ)
772+
msg = "should not do partial writes"
773+
with self.assertWarnsRegex(DeprecationWarning, msg):
774+
h.run(hello_app)
775+
self.assertEqual(b"HTTP/1.0 200 OK\r\n"
776+
b"Content-Type: text/plain\r\n"
777+
b"Date: Mon, 05 Jun 2006 18:49:54 GMT\r\n"
778+
b"Content-Length: 13\r\n"
779+
b"\r\n"
780+
b"Hello, world!",
781+
written)
782+
704783

705784
if __name__ == "__main__":
706785
unittest.main()

Lib/wsgiref/handlers.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,17 @@ def add_cgi_vars(self):
450450
self.environ.update(self.base_env)
451451

452452
def _write(self,data):
453-
self.stdout.write(data)
453+
result = self.stdout.write(data)
454+
if result is None or result == len(data):
455+
return
456+
from warnings import warn
457+
warn("SimpleHandler.stdout.write() should not do partial writes",
458+
DeprecationWarning)
459+
while True:
460+
data = data[result:]
461+
if not data:
462+
break
463+
result = self.stdout.write(data)
454464

455465
def _flush(self):
456466
self.stdout.flush()

Lib/wsgiref/simple_server.py

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

1313
from http.server import BaseHTTPRequestHandler, HTTPServer
14+
from io import BufferedWriter
1415
import sys
1516
import urllib.parse
1617
from wsgiref.handlers import SimpleHandler
@@ -126,11 +127,17 @@ def handle(self):
126127
if not self.parse_request(): # An error code has been sent, just exit
127128
return
128129

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())
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()
134141

135142

136143

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ Core and Builtins
2727
Library
2828
-------
2929

30+
- Issue #24291: Fix wsgiref.simple_server.WSGIRequestHandler to completely
31+
write data to the client. Previously it could do partial writes and
32+
truncate data. Also, wsgiref.handler.ServerHandler can now handle stdout
33+
doing partial writes, but this is deprecated.
34+
3035
- Issue #21272: Use _sysconfigdata.py to initialize distutils.sysconfig.
3136

3237
- Issue #19611: :mod:`inspect` now reports the implicit ``.0`` parameters

0 commit comments

Comments
 (0)