Repository: aio-libs/aiohttp

Issue:
There should be an exception in a case of "too many redirects"
## Long story short

When loading `http://relax-tip.de/` it redirects to `http://deepskin.de/`, but `http://deepskin.de/` redirects back to `http://relax-tip.de/`.

aiohttp has built-in limit `max_redirects=10`, but when this limit is reached users get last redirect response and no way to figure-out that is cycle redirect, as well aiohttp follows redirects automatically and it is no expected to get `304` response.

## Expected behaviour

I have an idea, when redirects limit is reached raise `TooManyRedirects` (requests do it)

## Actual behaviour

10 redirect response returned as successful response
 
## Steps to reproduce

```python
import aiohttp
import asyncio


async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get('http://relax-tip.de/') as response:
            import ipdb; ipdb.set_trace()
            response


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```

## Your environment

client

If such idea makes sense, I'll do pull request to implement this feature



Discussion:
Google chrome gets
```
This page isn’t working
relax-tip.de redirected you too many times.
Try clearing your cookies.
ERR_TOO_MANY_REDIRECTS
```
I think the same behavior in aiohttp makes sense.
Hi, i did some reading:
@hellysmile says that it should throw "TooManyRedirects" but according to wikipedia:
"The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes." 
i see problem with the end of that paragraph, because we are not doing rate-limiting here, no?

I think this kind of exception should be excpeted with 421 Misdirected Request:
https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#421

Maybe even to throw:
"404 Not Found
The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible."

Because in the end the "client/agent" did not "found" the resource in the end, right?

This is problem of Recursion: https://en.wikipedia.org/wiki/Recursion
as we started basically an infinite loop, we did stop it with request recurrence number limit doe, but still this is recursion, so following some Python patterns this custom exception would be named something alike: 
RequestRedirectRecursionError 
Or by looking into the aiohttp code:
ClientRedirectRecursionError to be more consistent.

But how to handle it is another story :), since in @hellysmile example in "Steps to reproduce" client managed to get response on the LAST recurrence can we say we got the resource?
I think not, and that is why i would treat this exception as "404 Not Found".

Draft:
class ClientRedirectRecursionError(ClientError):
    """ Recursion loop found in the request redirect cycle. """

Not sure how to handle it doe yet.

In my mind `404 Not Found` means exactly a server response with 404 status code.
But in case of too many redirects (it not necessary recursive redirections, maybe the chain is just longer than `max_redirects` parameter in aiohttp) status code for last response is 3XX. That's why a new `TooManyRedirects` exception should be added.
Perhaps `TooManyRedirects` should be derived from `aiohttp.ClientResponseError`
Agreed on 404 i went wrong in that direction.
So basically we talk here about two exceptions not one:

1. 
ClientRedirectRecursionError(ClientResponseError):
    """ Recursion loop found in the request redirect cycle. """
What if max_redirects would be big number here lets say > 2000 would you get RecurssionError
as in python intrepeter?

2.
ClientRedirectionTimeout(ClientResponseError):
   """ Client timed out waiting for the response. """
or
ClientRedirectionMaximumReached(ClientResponseError):
    """ Client has been redirected to many times without response. """
This exception in case of some small max_redirects, well that is a kind of timeout, and in this "timeout" the client did not manage to "find" response.
`requests` count a number of redirection limit (30 by default) and raises `requests.TooManyRedirects`.

I think the same behavior is good for aiohttp too (exception the limit is 10 for historical reasons, let's not touch it now).

Too high `max_redirects` is bad because a cost of HTTP request is much higher than cost of Python recursive call.

Redirection timeout is not what I want to have: the timeout depends on network speed and it interfere with timeout for single request. 
"Too high max_redirects is bad because a cost of HTTP request is much higher than cost of Python recursive call."
Yup, somebody could put max_redirects to big number by accident, for testing purposes...so what would/should happen then?
How about to limit that number to which it could be set and if somebody sets max_redirects over that number then you would raise ClientMaximumRedirectError("Too high max_redirects is bad because...") or issue a warning at least.

And in the case of reaching max_redirects:
ClientRedirectMaximumReached(ClientResponseError):
""" Client has been redirected to many times without response. """

No, it is overengineering. If somebody want to shoot the leg -- we do nothing.
Otherwise for consistency we should add such rings and bells (sane checks) to every parameter in every aiohttp call.

Resolving patch:
diff --git a/aiohttp/client.py b/aiohttp/client.py
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -18,7 +18,8 @@
 from . import hdrs, http, payload
 from .client_exceptions import *  # noqa
 from .client_exceptions import (ClientError, ClientOSError, InvalidURL,
-                                ServerTimeoutError, WSServerHandshakeError)
+                                ServerTimeoutError, TooManyRedirects,
+                                WSServerHandshakeError)
 from .client_reqrep import *  # noqa
 from .client_reqrep import ClientRequest, ClientResponse, _merge_ssl_params
 from .client_ws import ClientWebSocketResponse
@@ -360,7 +361,8 @@ async def _request(self, method, url, *,
                         history.append(resp)
                         if max_redirects and redirects >= max_redirects:
                             resp.close()
-                            break
+                            raise TooManyRedirects(
+                                history[0].request_info, tuple(history))
                         else:
                             resp.release()
 
diff --git a/aiohttp/client_exceptions.py b/aiohttp/client_exceptions.py
--- a/aiohttp/client_exceptions.py
+++ b/aiohttp/client_exceptions.py
@@ -93,6 +93,10 @@ class ClientHttpProxyError(ClientResponseError):
     """
 
 
+class TooManyRedirects(ClientResponseError):
+    """Client was redirected too many times."""
+
+
 class ClientConnectionError(ClientError):
     """Base class for client socket errors."""
 


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
@@ -16,6 +16,7 @@
 import aiohttp
 from aiohttp import Fingerprint, ServerFingerprintMismatch, hdrs, web
 from aiohttp.abc import AbstractResolver
+from aiohttp.client_exceptions import TooManyRedirects
 from aiohttp.test_utils import unused_port
 
 
@@ -914,10 +915,11 @@ async def redirect(request):
     app.router.add_get(r'/redirect/{count:\d+}', redirect)
     client = await aiohttp_client(app)
 
-    resp = await client.get('/redirect/5', max_redirects=2)
-    assert 302 == resp.status
-    assert 2 == len(resp.history)
-    resp.close()
+    with pytest.raises(TooManyRedirects) as ctx:
+        await client.get('/redirect/5', max_redirects=2)
+    assert 2 == len(ctx.value.history)
+    assert ctx.value.request_info.url.path == '/redirect/5'
+    assert ctx.value.request_info.method == 'GET'
 
 
 async def test_HTTP_200_GET_WITH_PARAMS(aiohttp_client):