diff options
| author | Jakub Stasiak <jakub@stasiak.at> | 2016-02-09 02:24:47 +0100 |
|---|---|---|
| committer | Jakub Stasiak <jakub@stasiak.at> | 2016-02-09 03:17:18 +0100 |
| commit | f011f783db1d9d58ee198dbef35fef9f9d4a6248 (patch) | |
| tree | ccf61f625ed2900f8071a6abad397db057b6c7ca | |
| parent | 8a4a1212b2097c6ecca96c81dc355388583ac45d (diff) | |
| download | eventlet-writelines-fix.tar.gz | |
wsgi: Fix handling partial writeswritelines-fix
Closes https://github.com/eventlet/eventlet/issues/295
| -rw-r--r-- | eventlet/support/__init__.py | 13 | ||||
| -rw-r--r-- | eventlet/wsgi.py | 7 | ||||
| -rw-r--r-- | tests/wsgi_test.py | 39 |
3 files changed, 56 insertions, 3 deletions
diff --git a/eventlet/support/__init__.py b/eventlet/support/__init__.py index 4c2b75d..de6b1e2 100644 --- a/eventlet/support/__init__.py +++ b/eventlet/support/__init__.py @@ -53,3 +53,16 @@ def capture_stderr(): finally: sys.stderr = original stream.seek(0) + + +if six.PY2: + def safe_writelines(fd, to_write): + fd.writelines(to_write) +else: + def safe_writelines(fd, to_write): + # Standard Python 3 writelines() is not reliable because it doesn't care if it + # loses data. See CPython bug report: http://bugs.python.org/issue26292 + for item in to_write: + written = 0 + while written < len(item): + written += fd.write(item[written:]) diff --git a/eventlet/wsgi.py b/eventlet/wsgi.py index 88e7752..66c7d4d 100644 --- a/eventlet/wsgi.py +++ b/eventlet/wsgi.py @@ -1,4 +1,5 @@ import errno +import functools import os import sys import time @@ -11,7 +12,7 @@ from eventlet.green import socket from eventlet import greenio from eventlet import greenpool from eventlet import support -from eventlet.support import six +from eventlet.support import safe_writelines, six from eventlet.support.six.moves import urllib @@ -112,7 +113,7 @@ class Input(object): # Blank line towrite.append(b'\r\n') - self.wfile.writelines(towrite) + safe_writelines(self.wfile, towrite) # Reinitialize chunk_length (expect more data) self.chunk_length = -1 @@ -385,7 +386,7 @@ class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler): length = [0] status_code = [200] - def write(data, _writelines=wfile.writelines): + def write(data, _writelines=functools.partial(safe_writelines, wfile)): towrite = [] if not headers_set: raise AssertionError("write() before start_response()") diff --git a/tests/wsgi_test.py b/tests/wsgi_test.py index 50091eb..2c25553 100644 --- a/tests/wsgi_test.py +++ b/tests/wsgi_test.py @@ -10,6 +10,8 @@ import tempfile import traceback import unittest +from nose.tools import eq_ + import eventlet from eventlet import debug from eventlet import event @@ -444,6 +446,43 @@ class TestHttpd(_TestBase): # Require a CRLF to close the message body self.assertEqual(response, b'\r\n') + def test_partial_writes_are_handled(self): + # The bug was caused by the default writelines() implementaiton + # (used by the wsgi module) which doesn't check if write() + # successfully completed sending *all* data therefore data could be + # lost and the client could be left hanging forever. + # + # Eventlet issue: "Python 3: wsgi doesn't handle correctly partial + # write of socket send() when using writelines()", + # https://github.com/eventlet/eventlet/issues/295 + # + # Related CPython issue: "Raw I/O writelines() broken", + # http://bugs.python.org/issue26292 + + # Piece of data large enough that we can reasonably expect it to not + # be sent in one piece. Is there a reliable way to determine this + # number without testing various sizes empirically? + data = b'a' * 10 ** 7 + + def application(env, start_response): + # Sending content-length is important here, otherwise chunked encoding + # is chosen + start_response('200 OK', []) + yield data + + self.site.application = application + + sock = eventlet.connect(('localhost', self.port)) + + # HTTP 1.0 is important here, this tells the server to close the socket + # after sending the data so that read_http() can safely read until EOF + # without hanging forever. + sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n') + result = read_http(sock) + + # This would previously fail because part of the data would be lost + eq_(len(result.body), len(data)) + @tests.skip_if_no_ssl def test_012_ssl_server(self): def wsgi_app(environ, start_response): |
