summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakub Stasiak <jakub@stasiak.at>2016-02-09 02:24:47 +0100
committerJakub Stasiak <jakub@stasiak.at>2016-02-09 03:17:18 +0100
commitf011f783db1d9d58ee198dbef35fef9f9d4a6248 (patch)
treeccf61f625ed2900f8071a6abad397db057b6c7ca
parent8a4a1212b2097c6ecca96c81dc355388583ac45d (diff)
downloadeventlet-writelines-fix.tar.gz
wsgi: Fix handling partial writeswritelines-fix
Closes https://github.com/eventlet/eventlet/issues/295
-rw-r--r--eventlet/support/__init__.py13
-rw-r--r--eventlet/wsgi.py7
-rw-r--r--tests/wsgi_test.py39
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):