Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#4587 Always check transport is not closing before reuse #4778

Merged
merged 5 commits into from
Oct 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGES/4587.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Always make sure transport is not closing before reuse a connection.

Reuse a protocol based on keepalive in headers is unreliable.
For example, uWSGI will not support keepalive even it serves a
http1.1 request, except explicitly configure uWSGI with a
`--http-keepalive` option.

Servers designed like uWSGI could cause aiohttp intermittently
raise a ConnectionResetException when the protocol poll runs
out and some protocol is reused.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ Konstantin Valetov
Kyrylo Perevozchikov
Kyungmin Lee
Lars P. Søndergaard
Liu Hua
Louis-Philippe Huberdeau
Loïc Lajeanne
Lu Gong
Expand Down
2 changes: 1 addition & 1 deletion aiohttp/client_proto.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def close(self) -> None:
self._drop_timeout()

def is_connected(self) -> bool:
return self.transport is not None
return self.transport is not None and not self.transport.is_closing()

def connection_lost(self, exc: Optional[BaseException]) -> None:
self._drop_timeout()
Expand Down
10 changes: 10 additions & 0 deletions aiohttp/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ def _cleanup(self) -> None:
transport)
else:
alive.append((proto, use_time))
else:
transport = proto.transport
proto.close()
if key.is_ssl and not self._cleanup_closed_disabled:
self._cleanup_closed_transports.append(transport)

if alive:
connections[key] = alive
Expand Down Expand Up @@ -552,6 +557,11 @@ def _get(self, key: 'ConnectionKey') -> Optional[ResponseHandler]:
# The very last connection was reclaimed: drop the key
del self._conns[key]
return proto
else:
transport = proto.transport
proto.close()
if key.is_ssl and not self._cleanup_closed_disabled:
self._cleanup_closed_transports.append(transport)

# No more connections: drop the key
del self._conns[key]
Expand Down
32 changes: 32 additions & 0 deletions tests/test_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,38 @@ async def test_get(loop) -> None:
await conn.close()


async def test_get_unconnected_proto(loop) -> None:
conn = aiohttp.BaseConnector()
key = ConnectionKey('localhost', 80, False, None, None, None, None)
assert conn._get(key) is None

proto = create_mocked_conn(loop)
conn._conns[key] = [(proto, loop.time())]
assert conn._get(key) == proto

assert conn._get(key) is None
conn._conns[key] = [(proto, loop.time())]
proto.is_connected = lambda *args: False
assert conn._get(key) is None
await conn.close()


async def test_get_unconnected_proto_ssl(loop) -> None:
conn = aiohttp.BaseConnector()
key = ConnectionKey('localhost', 80, True, None, None, None, None)
assert conn._get(key) is None

proto = create_mocked_conn(loop)
conn._conns[key] = [(proto, loop.time())]
assert conn._get(key) == proto

assert conn._get(key) is None
conn._conns[key] = [(proto, loop.time())]
proto.is_connected = lambda *args: False
assert conn._get(key) is None
await conn.close()


async def test_get_expired(loop) -> None:
conn = aiohttp.BaseConnector()
key = ConnectionKey('localhost', 80, False, None, None, None, None)
Expand Down