Repository: aio-libs/aiohttp

Issue:
AttributeError: 'NoneType' object has no attribute 'errno'
## Long story short

Trying to resolve a domain which is an alias for another one, which does not have an A or CNAME record, raises AttributeError: 'NoneType' object has no attribute 'errno'

## Expected behaviour

Raise an error correctly, socket.gaierror probably.

## Actual behaviour

```Traceback (most recent call last):
  File "xtest.py", line 16, in <module>
    process()
  File "/usr/lib/python3.6/asyncio/base_events.py", line 449, in run_until_complete
    return future.result()
  File "/usr/lib/python3.6/asyncio/tasks.py", line 239, in _step
    result = coro.send(None)
  File "/myenv/lib/python3.6/site-packages/aiohttp/helpers.py", line 72, in send
    return self._coro.send(arg)
  File "/myenv/lib/python3.6/site-packages/aiohttp/client.py", line 233, in _request
    conn = yield from self._connector.connect(req)
  File "/myenv/lib/python3.6/site-packages/aiohttp/connector.py", line 378, in connect
    proto = yield from self._create_connection(req)
  File "/myenv/lib/python3.6/site-packages/aiohttp/connector.py", line 687, in _create_connection
    _, proto = yield from self._create_direct_connection(req)
  File "/myenv/lib/python3.6/site-packages/aiohttp/connector.py", line 735, in _create_direct_connection
    exc.errno,
AttributeError: 'NoneType' object has no attribute 'errno'
```

## Steps to reproduce

This script will reproduce the error.

```
import asyncio
import aiohttp
from aiohttp.resolver import AsyncResolver

def process():
    url = 'http://esly.win/'
    resolver = AsyncResolver()
    conn = aiohttp.TCPConnector(resolver=resolver, verify_ssl=False)
    session = aiohttp.ClientSession(connector=conn)
    return session.get(url)

loop = asyncio.get_event_loop()
loop.run_until_complete(
    process()
)
```

If I use the session without setting the connector it first raises a socket.gaierror but then 
> During handling of the above exception, another exception occurred...

And the same traceback appears.

## Your environment
Python 3.6.0b2
Ubuntu 10.10
aiohttp==2.2,5 
Also happens with aiohttp==2.3.0a0 (installed from git on 29/Aug/2017)


Discussion:

Resolving patch:
diff --git a/aiohttp/resolver.py b/aiohttp/resolver.py
--- a/aiohttp/resolver.py
+++ b/aiohttp/resolver.py
@@ -60,31 +60,42 @@ def __init__(self, loop=None, *args, **kwargs):
 
         if not hasattr(self._resolver, 'gethostbyname'):
             # aiodns 1.1 is not available, fallback to DNSResolver.query
-            self.resolve = self.resolve_with_query
+            self.resolve = self._resolve_with_query
 
     @asyncio.coroutine
     def resolve(self, host, port=0, family=socket.AF_INET):
+        try:
+            resp = yield from self._resolver.gethostbyname(host, family)
+        except aiodns.error.DNSError as exc:
+            msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed"
+            raise OSError(msg) from exc
         hosts = []
-        resp = yield from self._resolver.gethostbyname(host, family)
-
         for address in resp.addresses:
             hosts.append(
                 {'hostname': host,
                  'host': address, 'port': port,
                  'family': family, 'proto': 0,
                  'flags': socket.AI_NUMERICHOST})
+
+        if not hosts:
+            raise OSError("DNS lookup failed")
+
         return hosts
 
     @asyncio.coroutine
-    def resolve_with_query(self, host, port=0, family=socket.AF_INET):
+    def _resolve_with_query(self, host, port=0, family=socket.AF_INET):
         if family == socket.AF_INET6:
             qtype = 'AAAA'
         else:
             qtype = 'A'
 
-        hosts = []
-        resp = yield from self._resolver.query(host, qtype)
+        try:
+            resp = yield from self._resolver.query(host, qtype)
+        except aiodns.error.DNSError as exc:
+            msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed"
+            raise OSError(msg) from exc
 
+        hosts = []
         for rr in resp:
             hosts.append(
                 {'hostname': host,
@@ -92,6 +103,9 @@ def resolve_with_query(self, host, port=0, family=socket.AF_INET):
                  'family': family, 'proto': 0,
                  'flags': socket.AI_NUMERICHOST})
 
+        if not hosts:
+            raise OSError("DNS lookup failed")
+
         return hosts
 
     @asyncio.coroutine


Test patch:
diff --git a/tests/test_resolver.py b/tests/test_resolver.py
--- a/tests/test_resolver.py
+++ b/tests/test_resolver.py
@@ -103,7 +103,7 @@ def test_async_resolver_negative_lookup(loop):
     with patch('aiodns.DNSResolver') as mock:
         mock().gethostbyname.side_effect = aiodns.error.DNSError()
         resolver = AsyncResolver(loop=loop)
-        with pytest.raises(aiodns.error.DNSError):
+        with pytest.raises(OSError):
             yield from resolver.resolve('doesnotexist.bla')
 
 
@@ -114,7 +114,28 @@ def test_async_resolver_query_negative_lookup(loop):
         del mock().gethostbyname
         mock().query.side_effect = aiodns.error.DNSError()
         resolver = AsyncResolver(loop=loop)
-        with pytest.raises(aiodns.error.DNSError):
+        with pytest.raises(OSError):
+            yield from resolver.resolve('doesnotexist.bla')
+
+
+@pytest.mark.skipif(aiodns is None, reason="aiodns required")
+@asyncio.coroutine
+def test_async_resolver_no_hosts_in_query(loop):
+    with patch('aiodns.DNSResolver') as mock:
+        del mock().gethostbyname
+        mock().query.return_value = fake_query_result([])
+        resolver = AsyncResolver(loop=loop)
+        with pytest.raises(OSError):
+            yield from resolver.resolve('doesnotexist.bla')
+
+
+@pytest.mark.skipif(not gethostbyname, reason="aiodns 1.1 required")
+@asyncio.coroutine
+def test_async_resolver_no_hosts_in_gethostbyname(loop):
+    with patch('aiodns.DNSResolver') as mock:
+        mock().gethostbyname.return_value = fake_result([])
+        resolver = AsyncResolver(loop=loop)
+        with pytest.raises(OSError):
             yield from resolver.resolve('doesnotexist.bla')