Repository: aio-libs/aiohttp

Issue:
Unretrieved exceptions [socket.gaierror]
## Long story short

<!-- Please describe your problem and why the fix is important. -->

When fetching a list of URLs, they are exceptions that are not retrieved by `aiohttp`. The ones that I came across are `socket.gaierror`.

## Expected behaviour

<!-- What is the behaviour you expect? -->

No exceptions.

## Actual behaviour

<!-- What's actually happening? -->

```python-traceback
http://aaal.club a
http://aaal.club b
http://aaal.club c
Task exception was never retrieved
future: <Task finished coro=<TCPConnector._resolve_host() done, defined at /home/s0h3ck/.virtualenvs/example/lib/python3.7/site-packages/aiohttp/connector.py:774> exception=gaierror(-3, 'Temporary failure in name resolution')>
Traceback (most recent call last):
  File "/home/s0h3ck/.virtualenvs/example/lib/python3.7/site-packages/aiohttp/connector.py", line 830, in _resolve_host
    self._resolver.resolve(host, port, family=self._family)
  File "/home/s0h3ck/.virtualenvs/example/lib/python3.7/site-packages/aiohttp/resolver.py", line 30, in resolve
    host, port, type=socket.SOCK_STREAM, family=family)
  File "/usr/lib64/python3.7/asyncio/base_events.py", line 784, in getaddrinfo
    None, getaddr_func, host, port, family, type, proto, flags)
  File "/usr/lib64/python3.7/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/usr/lib64/python3.7/socket.py", line 748, in getaddrinfo
    for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
socket.gaierror: [Errno -3] Temporary failure in name resolution

```

## Steps to reproduce

<!-- Please describe steps to reproduce the issue.
     If you have a script that does that please include it here within
     markdown code markup -->

1. Grab the following piece of code:

```python
import argparse
import asyncio
import logging
import aiohttp
import sys
​
log = logging.getLogger("script")
​
​
async def get_url(session, url):
    try:
        async with session.get(url, timeout=5) as response:
            content = await response.content.read()
            content = content.decode(errors="ignore")
            for pattern in ["a", "b", "c"]:
                if pattern in content:
                    print(url, pattern)
    except Exception as error:
        log.error(error)
        return False
    else:
        return True
​
​
async def process(urls):
    sem = asyncio.Semaphore(10)
​
    async with aiohttp.ClientSession() as session:
        for url in urls:
            async with sem:
                await get_url(session, url)
​
​
if __name__ == "__main__":
    log.addHandler(logging.StreamHandler(stream=sys.stderr))
    log.setLevel(logging.INFO)
​
    parser = argparse.ArgumentParser()
    parser.add_argument("urls", type=argparse.FileType(mode="r"))
    args = parser.parse_args()
​
    urls = [line.strip() for line in args.urls.read().split("\n") if line.strip()]
    log.info(f"Found {len(urls)} urls")
​
    asyncio.run(process(urls))
```

2. Grab the list of urls: `curl -L -o urls.txt "https://drive.google.com/uc?export=download&id=16XKCJWKGHoTOOLh69Cjxn31HE2CmZd9w"`

3. Execute the script: `python fetcher.py urls.txt`

## Your environment

<!-- Describe the environment you have that lead to your issue.
     This includes aiohttp version, OS, proxy server and other bits that
     are related to your case. 
     
     IMPORTANT: aiohttp is both server framework and client library.
     For getting rid of confusing please put 'server', 'client' or 'both'
     word here.
     -->

aiohttp==3.6.2
python 3.7




Discussion:
Thanks for the very good steps for the problem reproducing.
I see what you are talking on my laptop.
I came into the same problem in https://github.com/miguelgrinberg/python-socketio/issues/401.

Resolving patch:
diff --git a/aiohttp/connector.py b/aiohttp/connector.py
--- a/aiohttp/connector.py
+++ b/aiohttp/connector.py
@@ -954,18 +954,27 @@ async def _create_direct_connection(
         sslcontext = self._get_ssl_context(req)
         fingerprint = self._get_fingerprint(req)
 
+        host = req.url.raw_host
+        assert host is not None
+        port = req.port
+        assert port is not None
+        host_resolved = asyncio.ensure_future(self._resolve_host(
+            host,
+            port,
+            traces=traces), loop=self._loop)
         try:
             # Cancelling this lookup should not cancel the underlying lookup
             #  or else the cancel event will get broadcast to all the waiters
             #  across all connections.
-            host = req.url.raw_host
-            assert host is not None
-            port = req.port
-            assert port is not None
-            hosts = await asyncio.shield(self._resolve_host(
-                host,
-                port,
-                traces=traces))
+            hosts = await asyncio.shield(host_resolved)
+        except asyncio.CancelledError:
+            def drop_exception(
+                    fut: 'asyncio.Future[List[Dict[str, Any]]]'
+            ) -> None:
+                with suppress(Exception, asyncio.CancelledError):
+                    fut.result()
+            host_resolved.add_done_callback(drop_exception)
+            raise
         except OSError as exc:
             # in case of proxy it is not ClientProxyConnectionError
             # it is problem of resolving proxy ip itself


Test patch:
diff --git a/tests/test_connector.py b/tests/test_connector.py
--- a/tests/test_connector.py
+++ b/tests/test_connector.py
@@ -755,6 +755,50 @@ async def test_tcp_connector_dns_throttle_requests_cancelled_when_close(
             await f
 
 
+@pytest.fixture
+def dns_response_error(loop):
+    async def coro():
+        # simulates a network operation
+        await asyncio.sleep(0)
+        raise socket.gaierror(-3, 'Temporary failure in name resolution')
+    return coro
+
+
+async def test_tcp_connector_cancel_dns_error_captured(
+        loop,
+        dns_response_error) -> None:
+
+    exception_handler_called = False
+
+    def exception_handler(loop, context):
+        nonlocal exception_handler_called
+        exception_handler_called = True
+
+    loop.set_exception_handler(mock.Mock(side_effect=exception_handler))
+
+    with mock.patch('aiohttp.connector.DefaultResolver') as m_resolver:
+        req = ClientRequest(
+            method='GET',
+            url=URL('http://temporary-failure:80'),
+            loop=loop
+        )
+        conn = aiohttp.TCPConnector(
+            use_dns_cache=False,
+        )
+        m_resolver().resolve.return_value = dns_response_error()
+        f = loop.create_task(
+            conn._create_direct_connection(req, [], ClientTimeout(0))
+        )
+
+        await asyncio.sleep(0)
+        f.cancel()
+        with pytest.raises(asyncio.CancelledError):
+            await f
+
+        gc.collect()
+        assert exception_handler_called is False
+
+
 async def test_tcp_connector_dns_tracing(loop, dns_response) -> None:
     session = mock.Mock()
     trace_config_ctx = mock.Mock()