Repository: aio-libs/aiohttp

Issue:
Exception handling doesn't close connection
## Long story short

With certain malformed urls, the connection is not properly closed.   This causes an "Unclosed connection".

## Expected behaviour

Connection cleaned up when there is an exception writing headers.

## Actual behaviour

Connection not properly cleaned up.

## Steps to reproduce
Here are two examples, one mocked and one using a live site.  Both should produce an "Unclosed connection" error.

requires pytest and [aresponses](https://github.com/circleup/aresponses)
```python
@pytest.mark.asyncio
async def test_invalid_location(aresponses):
    # real example: 'http://www.skinhealthcanada.com/index.cfm?pagepath=Brands&id=66172'
    url = 'http://badwebsite.circleup.com/'
    bad_url = 'http://badwebsite.circleup.com/in\udcaedex.html'

    aresponses.add('badwebsite.circleup.com', aresponses.ANY, 'GET', aresponses.Response(status=302, headers={'Location': bad_url}))
    aresponses.add('badwebsite.circleup.com', aresponses.ANY, 'GET', aresponses.Response())

    async with aiohttp.ClientSession() as session:
        try:
            response = await session.get(bad_url)
        except Exception as e:
            print(repr(e))

    async with aiohttp.ClientSession() as session:
        response = await session.get(bad_url)


@pytest.mark.asyncio
async def test_invalid_location_live():
    url = 'http://www.skinhealthcanada.com/index.cfm?pagepath=Brands&id=66172'
    bad_url = 'http://www.skinhealthcanada.com/index.cfm?pagepath=Brands/Environ\udcae&id=66220'
    async with aiohttp.ClientSession() as session:
        try:
            response = await session.get(bad_url)
        except Exception as e:
            print(repr(e))

    async with aiohttp.ClientSession() as session:
        response = await session.get(bad_url)

```


## Your environment

client

Python 3.6.4
Ubuntu 17.10
virtualenv
aiohttp==3.0.6

## possible solution
change aiohttp/client.py lines 328-343 to
```python
                    try:
                        try:
                            resp = req.send(conn)
                        except BaseException:
                            conn.close()
                            raise
                        try:
                            await resp.start(conn, read_until_eof)
                        except BaseException:
                            resp.close()
                            conn.close()
                            raise
                    except ClientError:
                        raise
                    except OSError as exc:
                        raise ClientOSError(*exc.args) from exc
```


Discussion:

Resolving patch:
diff --git a/aiohttp/client.py b/aiohttp/client.py
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -326,11 +326,14 @@ async def _request(self, method, url, *,
                     tcp_nodelay(conn.transport, True)
                     tcp_cork(conn.transport, False)
                     try:
-                        resp = await req.send(conn)
                         try:
-                            await resp.start(conn, read_until_eof)
+                            resp = await req.send(conn)
+                            try:
+                                await resp.start(conn, read_until_eof)
+                            except BaseException:
+                                resp.close()
+                                raise
                         except BaseException:
-                            resp.close()
                             conn.close()
                             raise
                     except ClientError:


Test patch:
diff --git a/tests/test_client_session.py b/tests/test_client_session.py
--- a/tests/test_client_session.py
+++ b/tests/test_client_session.py
@@ -371,6 +371,7 @@ async def create_connection(req, traces=None):
         # return self.transport, self.protocol
         return mock.Mock()
     session._connector._create_connection = create_connection
+    session._connector._release = mock.Mock()
 
     with pytest.raises(aiohttp.ClientOSError) as ctx:
         await session.request('get', 'http://example.com')
@@ -379,6 +380,43 @@ async def create_connection(req, traces=None):
     assert e.strerror == err.strerror
 
 
+async def test_close_conn_on_error(create_session):
+    class UnexpectedException(BaseException):
+        pass
+
+    err = UnexpectedException("permission error")
+    req = mock.Mock()
+    req_factory = mock.Mock(return_value=req)
+    req.send = mock.Mock(side_effect=err)
+    session = create_session(request_class=req_factory)
+
+    connections = []
+    original_connect = session._connector.connect
+
+    async def connect(req, traces=None):
+        conn = await original_connect(req, traces=traces)
+        connections.append(conn)
+        return conn
+
+    async def create_connection(req, traces=None):
+        # return self.transport, self.protocol
+        conn = mock.Mock()
+        return conn
+
+    session._connector.connect = connect
+    session._connector._create_connection = create_connection
+    session._connector._release = mock.Mock()
+
+    with pytest.raises(UnexpectedException):
+        async with session.request('get', 'http://example.com') as resp:
+            await resp.text()
+
+    # normally called during garbage collection.  triggers an exception
+    # if the connection wasn't already closed
+    for c in connections:
+        c.__del__()
+
+
 async def test_cookie_jar_usage(loop, aiohttp_client):
     req_url = None