* drop RedirectLoop exception * tests is package to allow run it easly * bring back test for redirect loop
396 lines
15 KiB
Python
396 lines
15 KiB
Python
import json
|
|
from urllib.parse import parse_qs
|
|
|
|
import pytest
|
|
|
|
from httpx import (
|
|
URL,
|
|
AsyncClient,
|
|
NotRedirectResponse,
|
|
Request,
|
|
RequestBodyUnavailable,
|
|
Response,
|
|
TooManyRedirects,
|
|
codes,
|
|
)
|
|
from httpx._config import CertTypes, TimeoutTypes, VerifyTypes
|
|
from httpx._content_streams import AsyncIteratorStream
|
|
from httpx._dispatch.base import AsyncDispatcher
|
|
|
|
|
|
class MockDispatch(AsyncDispatcher):
|
|
async def send(
|
|
self,
|
|
request: Request,
|
|
verify: VerifyTypes = None,
|
|
cert: CertTypes = None,
|
|
timeout: TimeoutTypes = None,
|
|
) -> Response:
|
|
if request.url.path == "/no_redirect":
|
|
return Response(codes.OK, request=request)
|
|
|
|
elif request.url.path == "/redirect_301":
|
|
|
|
async def body():
|
|
yield b"<a href='https://example.org/'>here</a>"
|
|
|
|
status_code = codes.MOVED_PERMANENTLY
|
|
headers = {"location": "https://example.org/"}
|
|
stream = AsyncIteratorStream(aiterator=body())
|
|
return Response(
|
|
status_code, stream=stream, headers=headers, request=request
|
|
)
|
|
|
|
elif request.url.path == "/redirect_302":
|
|
status_code = codes.FOUND
|
|
headers = {"location": "https://example.org/"}
|
|
return Response(status_code, headers=headers, request=request)
|
|
|
|
elif request.url.path == "/redirect_303":
|
|
status_code = codes.SEE_OTHER
|
|
headers = {"location": "https://example.org/"}
|
|
return Response(status_code, headers=headers, request=request)
|
|
|
|
elif request.url.path == "/relative_redirect":
|
|
headers = {"location": "/"}
|
|
return Response(codes.SEE_OTHER, headers=headers, request=request)
|
|
|
|
elif request.url.path == "/malformed_redirect":
|
|
headers = {"location": "https://:443/"}
|
|
return Response(codes.SEE_OTHER, headers=headers, request=request)
|
|
|
|
elif request.url.path == "/no_scheme_redirect":
|
|
headers = {"location": "//example.org/"}
|
|
return Response(codes.SEE_OTHER, headers=headers, request=request)
|
|
|
|
elif request.url.path == "/multiple_redirects":
|
|
params = parse_qs(request.url.query)
|
|
count = int(params.get("count", "0")[0])
|
|
redirect_count = count - 1
|
|
code = codes.SEE_OTHER if count else codes.OK
|
|
location = "/multiple_redirects"
|
|
if redirect_count:
|
|
location += "?count=" + str(redirect_count)
|
|
headers = {"location": location} if count else {}
|
|
return Response(code, headers=headers, request=request)
|
|
|
|
if request.url.path == "/redirect_loop":
|
|
headers = {"location": "/redirect_loop"}
|
|
return Response(codes.SEE_OTHER, headers=headers, request=request)
|
|
|
|
elif request.url.path == "/cross_domain":
|
|
headers = {"location": "https://example.org/cross_domain_target"}
|
|
return Response(codes.SEE_OTHER, headers=headers, request=request)
|
|
|
|
elif request.url.path == "/cross_domain_target":
|
|
headers = dict(request.headers.items())
|
|
content = json.dumps({"headers": headers}).encode()
|
|
return Response(codes.OK, content=content, request=request)
|
|
|
|
elif request.url.path == "/redirect_body":
|
|
body = b"".join([part async for part in request.stream])
|
|
headers = {"location": "/redirect_body_target"}
|
|
return Response(codes.PERMANENT_REDIRECT, headers=headers, request=request)
|
|
|
|
elif request.url.path == "/redirect_no_body":
|
|
content = b"".join([part async for part in request.stream])
|
|
headers = {"location": "/redirect_body_target"}
|
|
return Response(codes.SEE_OTHER, headers=headers, request=request)
|
|
|
|
elif request.url.path == "/redirect_body_target":
|
|
content = b"".join([part async for part in request.stream])
|
|
headers = dict(request.headers.items())
|
|
body = json.dumps({"body": content.decode(), "headers": headers}).encode()
|
|
return Response(codes.OK, content=body, request=request)
|
|
|
|
elif request.url.path == "/cross_subdomain":
|
|
if request.headers["host"] != "www.example.org":
|
|
headers = {"location": "https://www.example.org/cross_subdomain"}
|
|
return Response(
|
|
codes.PERMANENT_REDIRECT, headers=headers, request=request
|
|
)
|
|
else:
|
|
return Response(codes.OK, content=b"Hello, world!", request=request)
|
|
|
|
return Response(codes.OK, content=b"Hello, world!", request=request)
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_no_redirect():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
url = "https://example.com/no_redirect"
|
|
response = await client.get(url)
|
|
assert response.status_code == 200
|
|
with pytest.raises(NotRedirectResponse):
|
|
await response.anext()
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_redirect_301():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
response = await client.post("https://example.org/redirect_301")
|
|
assert response.status_code == codes.OK
|
|
assert response.url == URL("https://example.org/")
|
|
assert len(response.history) == 1
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_redirect_302():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
response = await client.post("https://example.org/redirect_302")
|
|
assert response.status_code == codes.OK
|
|
assert response.url == URL("https://example.org/")
|
|
assert len(response.history) == 1
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_redirect_303():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
response = await client.get("https://example.org/redirect_303")
|
|
assert response.status_code == codes.OK
|
|
assert response.url == URL("https://example.org/")
|
|
assert len(response.history) == 1
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_disallow_redirects():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
response = await client.post(
|
|
"https://example.org/redirect_303", allow_redirects=False
|
|
)
|
|
assert response.status_code == codes.SEE_OTHER
|
|
assert response.url == URL("https://example.org/redirect_303")
|
|
assert response.is_redirect is True
|
|
assert len(response.history) == 0
|
|
|
|
response = await response.anext()
|
|
assert response.status_code == codes.OK
|
|
assert response.url == URL("https://example.org/")
|
|
assert response.is_redirect is False
|
|
assert len(response.history) == 1
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_relative_redirect():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
response = await client.get("https://example.org/relative_redirect")
|
|
assert response.status_code == codes.OK
|
|
assert response.url == URL("https://example.org/")
|
|
assert len(response.history) == 1
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_malformed_redirect():
|
|
# https://github.com/encode/httpx/issues/771
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
response = await client.get("http://example.org/malformed_redirect")
|
|
assert response.status_code == codes.OK
|
|
assert response.url == URL("https://example.org/")
|
|
assert len(response.history) == 1
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_no_scheme_redirect():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
response = await client.get("https://example.org/no_scheme_redirect")
|
|
assert response.status_code == codes.OK
|
|
assert response.url == URL("https://example.org/")
|
|
assert len(response.history) == 1
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_fragment_redirect():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
response = await client.get("https://example.org/relative_redirect#fragment")
|
|
assert response.status_code == codes.OK
|
|
assert response.url == URL("https://example.org/#fragment")
|
|
assert len(response.history) == 1
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_multiple_redirects():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
response = await client.get("https://example.org/multiple_redirects?count=20")
|
|
assert response.status_code == codes.OK
|
|
assert response.url == URL("https://example.org/multiple_redirects")
|
|
assert len(response.history) == 20
|
|
assert response.history[0].url == URL(
|
|
"https://example.org/multiple_redirects?count=20"
|
|
)
|
|
assert response.history[1].url == URL(
|
|
"https://example.org/multiple_redirects?count=19"
|
|
)
|
|
assert len(response.history[0].history) == 0
|
|
assert len(response.history[1].history) == 1
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_too_many_redirects():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
with pytest.raises(TooManyRedirects):
|
|
await client.get("https://example.org/multiple_redirects?count=21")
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_too_many_redirects_calling_next():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
url = "https://example.org/multiple_redirects?count=21"
|
|
response = await client.get(url, allow_redirects=False)
|
|
with pytest.raises(TooManyRedirects):
|
|
while response.is_redirect:
|
|
response = await response.anext()
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_redirect_loop():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
with pytest.raises(TooManyRedirects):
|
|
await client.get("https://example.org/redirect_loop")
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_cross_domain_redirect():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
url = "https://example.com/cross_domain"
|
|
headers = {"Authorization": "abc"}
|
|
response = await client.get(url, headers=headers)
|
|
assert response.url == URL("https://example.org/cross_domain_target")
|
|
assert "authorization" not in response.json()["headers"]
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_same_domain_redirect():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
url = "https://example.org/cross_domain"
|
|
headers = {"Authorization": "abc"}
|
|
response = await client.get(url, headers=headers)
|
|
assert response.url == URL("https://example.org/cross_domain_target")
|
|
assert response.json()["headers"]["authorization"] == "abc"
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_body_redirect():
|
|
"""
|
|
A 308 redirect should preserve the request body.
|
|
"""
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
url = "https://example.org/redirect_body"
|
|
data = b"Example request body"
|
|
response = await client.post(url, data=data)
|
|
assert response.url == URL("https://example.org/redirect_body_target")
|
|
assert response.json()["body"] == "Example request body"
|
|
assert "content-length" in response.json()["headers"]
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_no_body_redirect():
|
|
"""
|
|
A 303 redirect should remove the request body.
|
|
"""
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
url = "https://example.org/redirect_no_body"
|
|
data = b"Example request body"
|
|
response = await client.post(url, data=data)
|
|
assert response.url == URL("https://example.org/redirect_body_target")
|
|
assert response.json()["body"] == ""
|
|
assert "content-length" not in response.json()["headers"]
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_can_stream_if_no_redirect():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
url = "https://example.org/redirect_301"
|
|
async with client.stream("GET", url, allow_redirects=False) as response:
|
|
assert not response.is_closed
|
|
assert response.status_code == codes.MOVED_PERMANENTLY
|
|
assert response.headers["location"] == "https://example.org/"
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_cannot_redirect_streaming_body():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
url = "https://example.org/redirect_body"
|
|
|
|
async def streaming_body():
|
|
yield b"Example request body"
|
|
|
|
with pytest.raises(RequestBodyUnavailable):
|
|
await client.post(url, data=streaming_body())
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_cross_subdomain_redirect():
|
|
client = AsyncClient(dispatch=MockDispatch())
|
|
url = "https://example.com/cross_subdomain"
|
|
response = await client.get(url)
|
|
assert response.url == URL("https://www.example.org/cross_subdomain")
|
|
|
|
|
|
class MockCookieDispatch(AsyncDispatcher):
|
|
async def send(
|
|
self,
|
|
request: Request,
|
|
verify: VerifyTypes = None,
|
|
cert: CertTypes = None,
|
|
timeout: TimeoutTypes = None,
|
|
) -> Response:
|
|
if request.url.path == "/":
|
|
if "cookie" in request.headers:
|
|
content = b"Logged in"
|
|
else:
|
|
content = b"Not logged in"
|
|
return Response(codes.OK, content=content, request=request)
|
|
|
|
elif request.url.path == "/login":
|
|
status_code = codes.SEE_OTHER
|
|
headers = {
|
|
"location": "/",
|
|
"set-cookie": (
|
|
"session=eyJ1c2VybmFtZSI6ICJ0b21; path=/; Max-Age=1209600; "
|
|
"httponly; samesite=lax"
|
|
),
|
|
}
|
|
|
|
return Response(status_code, headers=headers, request=request)
|
|
|
|
elif request.url.path == "/logout":
|
|
status_code = codes.SEE_OTHER
|
|
headers = {
|
|
"location": "/",
|
|
"set-cookie": (
|
|
"session=null; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; "
|
|
"httponly; samesite=lax"
|
|
),
|
|
}
|
|
return Response(status_code, headers=headers, request=request)
|
|
|
|
|
|
@pytest.mark.usefixtures("async_environment")
|
|
async def test_redirect_cookie_behavior():
|
|
client = AsyncClient(dispatch=MockCookieDispatch())
|
|
|
|
# The client is not logged in.
|
|
response = await client.get("https://example.com/")
|
|
assert response.url == "https://example.com/"
|
|
assert response.text == "Not logged in"
|
|
|
|
# Login redirects to the homepage, setting a session cookie.
|
|
response = await client.post("https://example.com/login")
|
|
assert response.url == "https://example.com/"
|
|
assert response.text == "Logged in"
|
|
|
|
# The client is logged in.
|
|
response = await client.get("https://example.com/")
|
|
assert response.url == "https://example.com/"
|
|
assert response.text == "Logged in"
|
|
|
|
# Logout redirects to the homepage, expiring the session cookie.
|
|
response = await client.post("https://example.com/logout")
|
|
assert response.url == "https://example.com/"
|
|
assert response.text == "Not logged in"
|
|
|
|
# The client is not logged in.
|
|
response = await client.get("https://example.com/")
|
|
assert response.url == "https://example.com/"
|
|
assert response.text == "Not logged in"
|