Commit 6f734773 authored by Jason Madden's avatar Jason Madden

gevent.pywsgi: Support keep-alive in HTTP/1.0

With tests. Supercedes #1331.
parent 1d4899b8
......@@ -43,6 +43,9 @@
calling it, so ``unlink`` can sometimes be optimized out. See
:issue:`1487`.
- Make ``gevent.pywsgi`` support ``Connection: keep-alive`` in
HTTP/1.0. Based on :pr:`1331` by tanchuhan.
1.5a2 (2019-10-21)
==================
......
......@@ -555,7 +555,11 @@ class WSGIHandler(object):
if self.request_version == "HTTP/1.1":
conntype = self.headers.get("Connection", "").lower()
self.close_connection = (conntype == 'close')
elif self.request_version == 'HTTP/1.0':
conntype = self.headers.get("Connection", "close").lower()
self.close_connection = (conntype != 'keep-alive')
else:
# XXX: HTTP 0.9. We should drop support
self.close_connection = True
return True
......@@ -842,7 +846,7 @@ class WSGIHandler(object):
self.response_headers = response_headers
self.code = code
provided_connection = None
provided_connection = None # Did the wsgi app give us a Connection header?
self.provided_date = None
self.provided_content_length = None
......@@ -856,8 +860,8 @@ class WSGIHandler(object):
self.provided_content_length = value
if self.request_version == 'HTTP/1.0' and provided_connection is None:
response_headers.append((b'Connection', b'close'))
self.close_connection = True
conntype = b'close' if self.close_connection else b'keep-alive'
response_headers.append((b'Connection', conntype))
elif provided_connection == 'close':
self.close_connection = True
......
......@@ -315,8 +315,13 @@ class TestCase(greentest.TestCase):
return read_http(fd, *args, **kwargs)
HTTP_CLIENT_VERSION = '1.1'
DEFAULT_EXTRA_CLIENT_HEADERS = {}
def format_request(self, method='GET', path='/', **headers):
def_headers = self.DEFAULT_EXTRA_CLIENT_HEADERS.copy()
def_headers.update(headers)
headers = def_headers
headers = '\r\n'.join('%s: %s' % item for item in headers.items())
headers = headers + '\r\n' if headers else headers
result = (
......@@ -335,19 +340,28 @@ class TestCase(greentest.TestCase):
class CommonTestMixin(object):
PIPELINE_NOT_SUPPORTED_EXS = ()
EXPECT_CLOSE = False
EXPECT_KEEPALIVE = False
def test_basic(self):
with self.makefile() as fd:
fd.write(self.format_request())
response = read_http(fd, body='hello world')
if response.headers.get('Connection') == 'close':
return
self.assertTrue(self.EXPECT_CLOSE, "Server closed connection, not expecting that")
return response, None
self.assertFalse(self.EXPECT_CLOSE)
if self.EXPECT_KEEPALIVE:
response.assertHeader('Connection', 'keep-alive')
fd.write(self.format_request(path='/notexist'))
read_http(fd, code=404, reason='Not Found', body='not found')
dne_response = read_http(fd, code=404, reason='Not Found', body='not found')
fd.write(self.format_request())
read_http(fd, body='hello world')
response = read_http(fd, body='hello world')
return response, dne_response
def test_pipeline(self):
exception = AssertionError('HTTP pipelining not supported; the second request is thrown away')
......@@ -361,6 +375,8 @@ class CommonTestMixin(object):
read_http(fd, code=404, reason='Not Found', body='not found')
finally:
timeout.close()
except self.PIPELINE_NOT_SUPPORTED_EXS:
pass
except AssertionError as ex:
if ex is not exception:
raise
......@@ -370,7 +386,12 @@ class CommonTestMixin(object):
fd.write(self.format_request())
response = read_http(fd)
if response.headers.get('Connection') == 'close':
self.assertTrue(self.EXPECT_CLOSE, "Server closed connection, not expecting that")
return
self.assertFalse(self.EXPECT_CLOSE)
if self.EXPECT_KEEPALIVE:
response.assertHeader('Connection', 'keep-alive')
fd.write(self.format_request(Connection='close'))
read_http(fd)
fd.write(self.format_request())
......@@ -416,19 +437,32 @@ class TestNoChunks(CommonTestMixin, TestCase):
start_response('404 Not Found', [('Content-Type', 'text/plain')])
return [b'not ', b'found']
def test(self):
def test_basic(self):
response, dne_response = super(TestNoChunks, self).test_basic()
self.assertFalse(response.chunks)
response.assertHeader('Content-Length', '11')
if dne_response is not None:
self.assertFalse(dne_response.chunks)
dne_response.assertHeader('Content-Length', '9')
def test_dne(self):
with self.makefile() as fd:
fd.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
response = read_http(fd, body='hello world')
self.assertFalse(response.chunks)
response.assertHeader('Content-Length', '11')
fd.write('GET /not-found HTTP/1.1\r\nHost: localhost\r\n\r\n')
fd.write(self.format_request(path='/notexist'))
response = read_http(fd, code=404, reason='Not Found', body='not found')
self.assertFalse(response.chunks)
response.assertHeader('Content-Length', '9')
self.assertFalse(response.chunks)
response.assertHeader('Content-Length', '9')
class TestNoChunks10(TestNoChunks):
HTTP_CLIENT_VERSION = '1.0'
PIPELINE_NOT_SUPPORTED_EXS = (ConnectionClosed,)
EXPECT_CLOSE = True
class TestNoChunks10KeepAlive(TestNoChunks10):
DEFAULT_EXTRA_CLIENT_HEADERS = {
'Connection': 'keep-alive',
}
EXPECT_CLOSE = False
EXPECT_KEEPALIVE = True
class TestExplicitContentLength(TestNoChunks): # pylint:disable=too-many-ancestors
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment