From e1bc6282d470473867512eacfbfc235cf5b309cc Mon Sep 17 00:00:00 2001 From: Elizabeth Leddy Date: Mon, 13 Apr 2015 13:10:08 -0400 Subject: [PATCH 1/4] test cases for keep alive and http 1.0 --- tests/test_web_functional.py | 24 +++++++++++++++++++++++- tests/test_web_response.py | 20 +++++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index 7e2e4b1de62..98234759f7d 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -5,7 +5,7 @@ import unittest from aiohttp import web, request, FormData from aiohttp.multidict import MultiDict -from aiohttp.protocol import HttpVersion11 +from aiohttp.protocol import HttpVersion10, HttpVersion11 from aiohttp.streams import EOF_MARKER @@ -462,3 +462,25 @@ def go(): self.assertEqual(403, resp.status) self.loop.run_until_complete(go()) + + def test_http10_keep_alive(self): + + @asyncio.coroutine + def handler(request): + body = yield from request.read() + return web.Response(body=b'OK') + + @asyncio.coroutine + def go(): + _, _, url = yield from self.create_server('GET', '/', handler) + resp = yield from request('GET', url, loop=self.loop, + version=HttpVersion10) + self.assertEqual('close', resp.headers['CONNECTION']) + + _, _, url = yield from self.create_server('GET', '/', handler) + headers = {'Connection': 'keep-alive'} + resp = yield from request('GET', url, loop=self.loop, + headers=headers, version=HttpVersion10) + self.assertEqual('keep-alive', resp.headers['CONNECTION']) + + self.loop.run_until_complete(go()) diff --git a/tests/test_web_response.py b/tests/test_web_response.py index 0c6b1f4843a..b4ec1c1b9dd 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -4,7 +4,7 @@ from aiohttp import hdrs from aiohttp.multidict import CIMultiDict from aiohttp.web import Request, StreamResponse, Response -from aiohttp.protocol import RawRequestMessage, HttpVersion11 +from aiohttp.protocol import RawRequestMessage, HttpVersion11, HttpVersion10 class TestStreamResponse(unittest.TestCase): @@ -16,9 +16,10 @@ def setUp(self): def tearDown(self): self.loop.close() - def make_request(self, method, path, headers=CIMultiDict()): + def make_request(self, method, path, headers=CIMultiDict(), + version=HttpVersion11): self.app = mock.Mock() - message = RawRequestMessage(method, path, HttpVersion11, headers, + message = RawRequestMessage(method, path, version, headers, False, False) self.payload = mock.Mock() self.transport = mock.Mock() @@ -549,3 +550,16 @@ def test_text_in_ctor_with_content_type_header(self): self.assertEqual('текст'.encode('koi8-r'), resp.body) self.assertEqual('text/html', resp.content_type) self.assertEqual('koi8-r', resp.charset) + + def test_keep_alive_http10(self): + req = self.make_request('GET', '/', version=HttpVersion10) + resp = StreamResponse() + msg = resp.start(req) + self.assertEqual(resp.keep_alive, False) + + headers = CIMultiDict(Connection='keep-alive') + req = self.make_request('GET', '/', headers=headers, + version=HttpVersion10) + resp = StreamResponse() + msg = resp.start(req) + self.assertEqual(resp.keep_alive, True) From 8c1a372959c4c1e123ba64c0c4722050017f2d6e Mon Sep 17 00:00:00 2001 From: Elizabeth Leddy Date: Mon, 13 Apr 2015 13:11:29 -0400 Subject: [PATCH 2/4] ignore local venv setup variables --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 6414c1e2264..b89a8d9fa52 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.tar.gz *~ .DS_Store +.Python .coverage .idea .installed.cfg @@ -25,6 +26,9 @@ develop-eggs dist docs/_build/ eggs +include/ +lib/ +man/ nosetests.xml parts pyvenv From 141dcd73ee56951c5c7c0df2ced10495e923a4d7 Mon Sep 17 00:00:00 2001 From: Elizabeth Leddy Date: Tue, 14 Apr 2015 09:59:11 -0400 Subject: [PATCH 3/4] support keep-alive for http 1.0 keep alive is on by default for http 1.1 and off by default for http 1.0. parse connection headers for 1.0 and obey --- aiohttp/protocol.py | 32 +++++++++++++++++------------ aiohttp/web_reqrep.py | 10 ++------- tests/test_web_functional.py | 32 +++++++++++++++++++++++++++-- tests/test_web_request.py | 2 ++ tests/test_web_response.py | 39 ++++++++++++++++++++---------------- 5 files changed, 75 insertions(+), 40 deletions(-) diff --git a/aiohttp/protocol.py b/aiohttp/protocol.py index be9aac41bbb..ae2baa32ca0 100644 --- a/aiohttp/protocol.py +++ b/aiohttp/protocol.py @@ -186,10 +186,11 @@ def __call__(self, out, buf): # read headers headers, close, compression = self.parse_headers(lines) - if version <= HttpVersion10: - close = True - elif close is None: - close = False + if close is None: # then the headers weren't set in the request + if version <= HttpVersion10: # HTTP 1.0 must asks to not close + close = True + else: # HTTP 1.1 must ask to close. + close = False out.feed_data( RawRequestMessage( @@ -532,13 +533,7 @@ def __init__(self, transport, version, close): self.transport = transport self.version = version self.closing = close - - # disable keep-alive for http/1.0 - if version <= HttpVersion10: - self.keepalive = False - else: - self.keepalive = None - + self.keepalive = None self.chunked = False self.length = None self.headers = CIMultiDict() @@ -555,7 +550,13 @@ def enable_chunked_encoding(self): def keep_alive(self): if self.keepalive is None: - return not self.closing + if self.version <= HttpVersion10: + if self.headers.get('Connection') == 'keep-alive': + return True + else: # no headers means we close for Http 1.0 + return False + else: + return not self.closing else: return self.keepalive @@ -591,7 +592,7 @@ def add_header(self, name, value): # connection keep-alive elif 'close' in val: self.keepalive = False - elif 'keep-alive' in val and self.version >= HttpVersion11: + elif 'keep-alive' in val: self.keepalive = True elif name == hdrs.UPGRADE: @@ -836,6 +837,11 @@ class Request(HttpMessage): def __init__(self, transport, method, path, http_version=HttpVersion11, close=False): + # set the default for HTTP 1.0 to be different + # will only be overwritten with keep-alive header + if http_version < HttpVersion11: + close = True + super().__init__(transport, http_version, close) self.method = method diff --git a/aiohttp/web_reqrep.py b/aiohttp/web_reqrep.py index f298d414d66..556294d837b 100644 --- a/aiohttp/web_reqrep.py +++ b/aiohttp/web_reqrep.py @@ -18,7 +18,7 @@ CIMultiDict, MultiDictProxy, MultiDict) -from .protocol import Response as ResponseImpl, HttpVersion11 +from .protocol import Response as ResponseImpl from .streams import EOF_MARKER @@ -95,13 +95,7 @@ def __init__(self, app, message, payload, transport, reader, writer, *, self._post = None self._post_files_cache = None self._headers = CIMultiDictProxy(message.headers) - - if self._version < HttpVersion11: - self._keep_alive = False - elif message.should_close: - self._keep_alive = False - else: - self._keep_alive = True + self._keep_alive = not message.should_close # matchdict, route_name, handler # or information about traversal lookup diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index 98234759f7d..ca2a42537ea 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -463,11 +463,11 @@ def go(): self.loop.run_until_complete(go()) - def test_http10_keep_alive(self): + def test_http10_keep_alive_default(self): @asyncio.coroutine def handler(request): - body = yield from request.read() + yield from request.read() return web.Response(body=b'OK') @asyncio.coroutine @@ -477,6 +477,34 @@ def go(): version=HttpVersion10) self.assertEqual('close', resp.headers['CONNECTION']) + self.loop.run_until_complete(go()) + + def test_http10_keep_alive_with_headers_close(self): + + @asyncio.coroutine + def handler(request): + yield from request.read() + return web.Response(body=b'OK') + + @asyncio.coroutine + def go(): + _, _, url = yield from self.create_server('GET', '/', handler) + headers = {'Connection': 'close'} + resp = yield from request('GET', url, loop=self.loop, + headers=headers, version=HttpVersion10) + self.assertEqual('close', resp.headers['CONNECTION']) + + self.loop.run_until_complete(go()) + + def test_http10_keep_alive_with_headers(self): + + @asyncio.coroutine + def handler(request): + yield from request.read() + return web.Response(body=b'OK') + + @asyncio.coroutine + def go(): _, _, url = yield from self.create_server('GET', '/', handler) headers = {'Connection': 'keep-alive'} resp = yield from request('GET', url, loop=self.loop, diff --git a/tests/test_web_request.py b/tests/test_web_request.py index eddcb3eb78f..257e2124262 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -18,6 +18,8 @@ def tearDown(self): def make_request(self, method, path, headers=CIMultiDict(), *, version=HttpVersion(1, 1), closing=False): + if version < HttpVersion(1, 1): + closing = True self.app = mock.Mock() message = RawRequestMessage(method, path, version, headers, closing, False) diff --git a/tests/test_web_response.py b/tests/test_web_response.py index b4ec1c1b9dd..fea7cbac9d1 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -16,11 +16,13 @@ def setUp(self): def tearDown(self): self.loop.close() - def make_request(self, method, path, headers=CIMultiDict(), - version=HttpVersion11): - self.app = mock.Mock() - message = RawRequestMessage(method, path, version, headers, + def make_request(self, method, path, headers=CIMultiDict()): + message = RawRequestMessage(method, path, HttpVersion11, headers, False, False) + return self.request_from_message(message) + + def request_from_message(self, message): + self.app = mock.Mock() self.payload = mock.Mock() self.transport = mock.Mock() self.reader = mock.Mock() @@ -348,6 +350,22 @@ def test___repr__not_started(self): resp = StreamResponse(reason=301) self.assertEqual("", repr(resp)) + def test_keep_alive_http10(self): + message = RawRequestMessage('GET', '/', HttpVersion10, CIMultiDict(), + True, False) + req = self.request_from_message(message) + resp = StreamResponse() + resp.start(req) + self.assertEqual(resp.keep_alive, False) + + headers = CIMultiDict(Connection='keep-alive') + message = RawRequestMessage('GET', '/', HttpVersion10, headers, + False, False) + req = self.request_from_message(message) + resp = StreamResponse() + resp.start(req) + self.assertEqual(resp.keep_alive, True) + class TestResponse(unittest.TestCase): @@ -550,16 +568,3 @@ def test_text_in_ctor_with_content_type_header(self): self.assertEqual('текст'.encode('koi8-r'), resp.body) self.assertEqual('text/html', resp.content_type) self.assertEqual('koi8-r', resp.charset) - - def test_keep_alive_http10(self): - req = self.make_request('GET', '/', version=HttpVersion10) - resp = StreamResponse() - msg = resp.start(req) - self.assertEqual(resp.keep_alive, False) - - headers = CIMultiDict(Connection='keep-alive') - req = self.make_request('GET', '/', headers=headers, - version=HttpVersion10) - resp = StreamResponse() - msg = resp.start(req) - self.assertEqual(resp.keep_alive, True) From 6c3f1f814ddd6239420d09726b1433aa7acc7adc Mon Sep 17 00:00:00 2001 From: Elizabeth Leddy Date: Tue, 14 Apr 2015 11:22:37 -0400 Subject: [PATCH 4/4] always close connection for http < 1.0 --- aiohttp/protocol.py | 7 +++++-- aiohttp/web_reqrep.py | 7 +++++-- tests/test_web_functional.py | 20 +++++++++++++++++++- tests/test_web_response.py | 14 ++++++++++++-- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/aiohttp/protocol.py b/aiohttp/protocol.py index ae2baa32ca0..08d6dbd89cd 100644 --- a/aiohttp/protocol.py +++ b/aiohttp/protocol.py @@ -550,8 +550,11 @@ def enable_chunked_encoding(self): def keep_alive(self): if self.keepalive is None: - if self.version <= HttpVersion10: - if self.headers.get('Connection') == 'keep-alive': + if self.version < HttpVersion10: + # keep alive not supported at all + return False + if self.version == HttpVersion10: + if self.headers.get(hdrs.CONNECTION) == 'keep-alive': return True else: # no headers means we close for Http 1.0 return False diff --git a/aiohttp/web_reqrep.py b/aiohttp/web_reqrep.py index 556294d837b..0cf0919ab1e 100644 --- a/aiohttp/web_reqrep.py +++ b/aiohttp/web_reqrep.py @@ -18,7 +18,7 @@ CIMultiDict, MultiDictProxy, MultiDict) -from .protocol import Response as ResponseImpl +from .protocol import Response as ResponseImpl, HttpVersion10 from .streams import EOF_MARKER @@ -95,7 +95,10 @@ def __init__(self, app, message, payload, transport, reader, writer, *, self._post = None self._post_files_cache = None self._headers = CIMultiDictProxy(message.headers) - self._keep_alive = not message.should_close + if self._version < HttpVersion10: + self._keep_alive = False + else: + self._keep_alive = not message.should_close # matchdict, route_name, handler # or information about traversal lookup diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index ca2a42537ea..ab0b46cb88c 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -5,7 +5,7 @@ import unittest from aiohttp import web, request, FormData from aiohttp.multidict import MultiDict -from aiohttp.protocol import HttpVersion10, HttpVersion11 +from aiohttp.protocol import HttpVersion, HttpVersion10, HttpVersion11 from aiohttp.streams import EOF_MARKER @@ -479,6 +479,24 @@ def go(): self.loop.run_until_complete(go()) + def test_http09_keep_alive_default(self): + + @asyncio.coroutine + def handler(request): + yield from request.read() + return web.Response(body=b'OK') + + @asyncio.coroutine + def go(): + headers = {'Connection': 'keep-alive'} # should be ignored + _, _, url = yield from self.create_server('GET', '/', handler) + resp = yield from request('GET', url, loop=self.loop, + headers=headers, + version=HttpVersion(0, 9)) + self.assertEqual('close', resp.headers['CONNECTION']) + + self.loop.run_until_complete(go()) + def test_http10_keep_alive_with_headers_close(self): @asyncio.coroutine diff --git a/tests/test_web_response.py b/tests/test_web_response.py index fea7cbac9d1..522fb458f02 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -4,7 +4,8 @@ from aiohttp import hdrs from aiohttp.multidict import CIMultiDict from aiohttp.web import Request, StreamResponse, Response -from aiohttp.protocol import RawRequestMessage, HttpVersion11, HttpVersion10 +from aiohttp.protocol import HttpVersion, HttpVersion11, HttpVersion10 +from aiohttp.protocol import RawRequestMessage class TestStreamResponse(unittest.TestCase): @@ -356,7 +357,7 @@ def test_keep_alive_http10(self): req = self.request_from_message(message) resp = StreamResponse() resp.start(req) - self.assertEqual(resp.keep_alive, False) + self.assertFalse(resp.keep_alive) headers = CIMultiDict(Connection='keep-alive') message = RawRequestMessage('GET', '/', HttpVersion10, headers, @@ -366,6 +367,15 @@ def test_keep_alive_http10(self): resp.start(req) self.assertEqual(resp.keep_alive, True) + def test_keep_alive_http09(self): + headers = CIMultiDict(Connection='keep-alive') + message = RawRequestMessage('GET', '/', HttpVersion(0, 9), headers, + False, False) + req = self.request_from_message(message) + resp = StreamResponse() + resp.start(req) + self.assertFalse(resp.keep_alive) + class TestResponse(unittest.TestCase):