from urllib.parse import urljoin, urlparse
from ..config import DEFAULT_MAX_REDIRECTS
-from ..exceptions import TooManyRedirects
+from ..exceptions import RedirectLoop, TooManyRedirects
from ..interfaces import Adapter
from ..models import URL, Request, Response
from ..status_codes import codes
async def send(self, request: Request, **options: typing.Any) -> Response:
allow_redirects = options.pop("allow_redirects", True)
history = []
+ seen_urls = set((request.url,))
while True:
response = await self.dispatch.send(request, **options)
if len(history) > self.max_redirects:
raise TooManyRedirects()
request = self.build_redirect_request(request, response)
+ if request.url in seen_urls:
+ raise RedirectLoop()
+ seen_urls.add(request.url)
return response
-import pytest
from urllib.parse import parse_qs
-from httpcore import Adapter, RedirectAdapter, Request, Response, TooManyRedirects, codes
+import pytest
+
+from httpcore import (
+ Adapter,
+ RedirectAdapter,
+ RedirectLoop,
+ Request,
+ Response,
+ TooManyRedirects,
+ codes,
+)
class MockDispatch(Adapter):
pass
async def send(self, request: Request, **options) -> Response:
- if request.url.path == "/redirect_303":
- return Response(
- codes.see_other, headers=[(b"location", b"https://example.org/")]
- )
+ if request.url.path == "/redirect_301": # "Moved Permanently"
+ return Response(301, headers=[(b"location", b"https://example.org/")])
+
+ elif request.url.path == "/redirect_302": # "Found"
+ return Response(302, headers=[(b"location", b"https://example.org/")])
+
+ elif request.url.path == "/redirect_303": # "See Other"
+ return Response(303, headers=[(b"location", b"https://example.org/")])
+
elif request.url.path == "/relative_redirect":
return Response(codes.see_other, headers=[(b"location", b"/")])
+
+ elif request.url.path == "/no_scheme_redirect":
+ return Response(codes.see_other, headers=[(b"location", b"//example.org/")])
+
elif request.url.path == "/multiple_redirects":
params = parse_qs(request.url.query)
count = int(params.get("count", "0")[0])
location = "/multiple_redirects?count=" + str(count - 1)
headers = [(b"location", location.encode())] if count else []
return Response(code, headers=headers)
+
+ if request.url.path == "/redirect_loop":
+ return Response(codes.see_other, headers=[(b"location", b"/redirect_loop")])
+
return Response(codes.ok, body=b"Hello, world!")
+@pytest.mark.asyncio
+async def test_redirect_301():
+ client = RedirectAdapter(MockDispatch())
+ response = await client.request("POST", "https://example.org/redirect_301")
+ assert response.status_code == codes.ok
+
+
+@pytest.mark.asyncio
+async def test_redirect_302():
+ client = RedirectAdapter(MockDispatch())
+ response = await client.request("POST", "https://example.org/redirect_302")
+ assert response.status_code == codes.ok
+
+
@pytest.mark.asyncio
async def test_redirect_303():
client = RedirectAdapter(MockDispatch())
assert response.status_code == codes.ok
+@pytest.mark.asyncio
+async def test_no_scheme_redirect():
+ client = RedirectAdapter(MockDispatch())
+ response = await client.request("GET", "https://example.org/no_scheme_redirect")
+ assert response.status_code == codes.ok
+
+
+@pytest.mark.asyncio
+async def test_fragment_redirect():
+ client = RedirectAdapter(MockDispatch())
+ response = await client.request("GET", "https://example.org/relative_redirect#fragment")
+ assert response.status_code == codes.ok
+
+
@pytest.mark.asyncio
async def test_multiple_redirects():
client = RedirectAdapter(MockDispatch())
- response = await client.request("GET", "https://example.org/multiple_redirects?count=20")
+ response = await client.request(
+ "GET", "https://example.org/multiple_redirects?count=20"
+ )
assert response.status_code == codes.ok
client = RedirectAdapter(MockDispatch())
with pytest.raises(TooManyRedirects):
await client.request("GET", "https://example.org/multiple_redirects?count=21")
+
+
+@pytest.mark.asyncio
+async def test_redirect_loop():
+ client = RedirectAdapter(MockDispatch())
+ with pytest.raises(RedirectLoop):
+ await client.request("GET", "https://example.org/redirect_loop")