diff --git a/src/httpx2/httpx2/_client.py b/src/httpx2/httpx2/_client.py index ae73434e..1c6276f4 100644 --- a/src/httpx2/httpx2/_client.py +++ b/src/httpx2/httpx2/_client.py @@ -1054,13 +1054,18 @@ def _send_handling_redirects( if not response.has_redirect_location: return response - request = self._build_redirect_request(request, response) - history = history + [response] - if follow_redirects: + request = self._build_redirect_request(request, response) + history = history + [response] response.read() else: - response.next_request = request + # When not following redirects, building the next request is + # best-effort: a malformed 'Location' must not discard the + # response and its headers. Leave next_request as None. + try: + response.next_request = self._build_redirect_request(request, response) + except (InvalidURL, RemoteProtocolError): + pass return response except BaseException as exc: @@ -1892,13 +1897,18 @@ async def _send_handling_redirects( if not response.has_redirect_location: return response - request = self._build_redirect_request(request, response) - history = history + [response] - if follow_redirects: + request = self._build_redirect_request(request, response) + history = history + [response] await response.aread() else: - response.next_request = request + # When not following redirects, building the next request is + # best-effort: a malformed 'Location' must not discard the + # response and its headers. Leave next_request as None. + try: + response.next_request = self._build_redirect_request(request, response) + except (InvalidURL, RemoteProtocolError): + pass return response except BaseException as exc: diff --git a/tests/httpx2/client/test_redirects.py b/tests/httpx2/client/test_redirects.py index 0dee968c..e7f434c5 100644 --- a/tests/httpx2/client/test_redirects.py +++ b/tests/httpx2/client/test_redirects.py @@ -208,6 +208,17 @@ def test_invalid_redirect() -> None: client.get("http://example.org/invalid_redirect", follow_redirects=True) +def test_invalid_redirect_not_followed() -> None: + # A malformed 'Location' must not discard the response when we are not + # following redirects: the response and its headers are returned as-is, + # with next_request left as None. + client = httpx2.Client(transport=httpx2.MockTransport(redirects)) + response = client.get("http://example.org/invalid_redirect", follow_redirects=False) + assert response.status_code == httpx2.codes.SEE_OTHER + assert response.headers["location"] == "https://😇/" + assert response.next_request is None + + def test_no_scheme_redirect() -> None: client = httpx2.Client(transport=httpx2.MockTransport(redirects)) response = client.get("https://example.org/no_scheme_redirect", follow_redirects=True) @@ -458,3 +469,12 @@ async def test_async_invalid_redirect() -> None: async with httpx2.AsyncClient(transport=httpx2.MockTransport(redirects)) as client: with pytest.raises(httpx2.RemoteProtocolError): await client.get("http://example.org/invalid_redirect", follow_redirects=True) + + +@pytest.mark.anyio +async def test_async_invalid_redirect_not_followed() -> None: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(redirects)) as client: + response = await client.get("http://example.org/invalid_redirect", follow_redirects=False) + assert response.status_code == httpx2.codes.SEE_OTHER + assert response.headers["location"] == "https://😇/" + assert response.next_request is None