Repository: aio-libs/aiohttp

Issue:
RuntimeError raised when a Redirect doesn't have a Location or URI HTTP header
## Long story short

As per title, whenever a redirect response doesn't have a Location or a URI HTTP header, a RuntimeError will be raised by aiohttp. While this is indeed a problem, I think that raising RuntimeError seems a bit too much, since now I have to handle it explicitly in my code, rather than catching an aiohttp exception

## Expected behaviour

For another exception, particular to aiohttp, to be raised, so that we can handle this case from our code, without hiding other bugs.

## Actual behaviour

RuntimeError gets raised

## Steps to reproduce

```
import aiohttp
import asyncio

async def fetch(session, url):
        async with session.get(url) as response:
            return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'http://www.droidreport.com/googles-market-android-wearables-7173')
        print(html)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```

## Your environment

aiohttp==2.0.7
Python 3.6



Discussion:
Well, a new class in `http_exceptions.py` derived from `BadHttpMessage` sounds like a good solution.

Would you make a PR?
Sure!
Please note: procedure for adding changes to `CHANGES.rst` will be changed on #2010 merging.
I expect to finish the work in a few hours

Resolving patch:
diff --git a/aiohttp/client.py b/aiohttp/client.py
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -26,6 +26,7 @@
 from .helpers import (PY_35, CeilTimeout, TimeoutHandle, deprecated_noop,
                       sentinel)
 from .http import WS_KEY, WebSocketReader, WebSocketWriter
+from .http_exceptions import RedirectURLError
 from .streams import FlowControlDataQueue
 
 
@@ -274,7 +275,7 @@ def _request(self, method, url, *,
                         r_url = (resp.headers.get(hdrs.LOCATION) or
                                  resp.headers.get(hdrs.URI))
                         if r_url is None:
-                            raise RuntimeError(
+                            raise RedirectURLError(
                                 "{0.method} {0.url} returns "
                                 "a redirect [{0.status}] status "
                                 "but response lacks a Location "
diff --git a/aiohttp/http_exceptions.py b/aiohttp/http_exceptions.py
--- a/aiohttp/http_exceptions.py
+++ b/aiohttp/http_exceptions.py
@@ -57,6 +57,10 @@ class ContentLengthError(PayloadEncodingError):
     """Not enough data for satisfy content length header."""
 
 
+class RedirectURLError(BadHttpMessage):
+    """A redirect response lacks a Location or URI HTTP header"""
+
+
 class LineTooLong(BadHttpMessage):
 
     def __init__(self, line, limit='Unknown'):


Test patch:
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -15,6 +15,7 @@
 from aiohttp import hdrs, web
 from aiohttp.client import ServerFingerprintMismatch
 from aiohttp.helpers import create_future
+from aiohttp.http_exceptions import RedirectURLError
 from aiohttp.multipart import MultipartWriter
 
 
@@ -2106,12 +2107,13 @@ def handler_redirect(request):
     app.router.add_route('GET', '/redirect', handler_redirect)
     client = yield from test_client(app)
 
-    with pytest.raises(RuntimeError) as ctx:
+    with pytest.raises(RedirectURLError) as ctx:
         yield from client.get('/redirect')
-    assert str(ctx.value) == ('GET http://127.0.0.1:{}/redirect returns '
-                              'a redirect [301] status but response lacks '
-                              'a Location or URI HTTP header'
-                              .format(client.port))
+    expected_msg = ('GET http://127.0.0.1:{}/redirect returns '
+                    'a redirect [301] status but response lacks '
+                    'a Location or URI HTTP header'
+                    .format(client.port))
+    assert str(ctx.value.message) == expected_msg
 
 
 @asyncio.coroutine