wsgi: suppress output of 0-byte chunks

Under certain conditions, if the WSGI iterable yields an empty string,
it can cause the response to be terminated.

The conditions are as follows:

  * no minimum chunk size (eventlet.wsgi.server() was called with
    minimum_chunk_size=0, or
    env['eventlet.minimum_write_chunk_size'] = 0)

  * chunked transfer-encoding on the response

In this case, if the WSGI iterable yields an empty string, then
eventlet.wsgi obligingly turns that into "0\r\n\r\n" and writes that
to the socket. This, of course, terminates the response as far as the
client is concerned. However, eventlet.wsgi doesn't notice, so it'll
happily keep calling next(app_iter) and sending the chunks.

If Connection: keep-alive is set, the client might then send the next
request, read some chunked response body, fail to parse it, and then
disconnect.

In other cases, either the 0-byte chunk is saved in the chunk buffer
until the minimum chunk size is met, or 0 bytes get written to the
socket. Those are both no-ops.

This commit discards 0-byte chunks from the WSGI iterable.
This commit is contained in:
Samuel Merritt
2015-10-08 16:05:57 -07:00
parent 5a4a7ead24
commit f1e4d33d02
2 changed files with 17 additions and 1 deletions

View File

@@ -485,6 +485,7 @@ class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler):
towrite.append(data) towrite.append(data)
towrite_size += len(data) towrite_size += len(data)
if towrite_size >= minimum_write_chunk_size: if towrite_size >= minimum_write_chunk_size:
if towrite_size > 0:
write(b''.join(towrite)) write(b''.join(towrite))
towrite = [] towrite = []
just_written_size = towrite_size just_written_size = towrite_size

View File

@@ -1048,6 +1048,21 @@ class TestHttpd(_TestBase):
self.assertNotEqual(result.headers_lower.get('transfer-encoding'), 'chunked') self.assertNotEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
self.assertEqual(result.body, b"thisischunked") self.assertEqual(result.body, b"thisischunked")
def test_chunked_response_when_app_yields_empty_string(self):
def empty_string_chunked_app(env, start_response):
env['eventlet.minimum_write_chunk_size'] = 0 # no buffering
start_response('200 OK', [('Content-type', 'text/plain')])
return iter([b"stuff", b"", b"more stuff"])
self.site.application = empty_string_chunked_app
sock = eventlet.connect(('localhost', self.port))
sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
result = read_http(sock)
self.assertEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
self.assertEqual(result.body, b"5\r\nstuff\r\na\r\nmore stuff\r\n0\r\n\r\n")
def test_minimum_chunk_size_parameter_leaves_httpprotocol_class_member_intact(self): def test_minimum_chunk_size_parameter_leaves_httpprotocol_class_member_intact(self):
start_size = wsgi.HttpProtocol.minimum_chunk_size start_size = wsgi.HttpProtocol.minimum_chunk_size