From 343255fc481ecd95dd46a9cdf686de8dcd697b6a Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 29 Jun 2026 13:22:28 +1000 Subject: [PATCH] requests: Add support for relative redirect URLs. A redirection URL in the "Location:" header may be relative and start with a "/". Handle such cases. Signed-off-by: Damien George --- python-ecosys/requests/manifest.py | 2 +- python-ecosys/requests/requests/__init__.py | 2 + python-ecosys/requests/test_requests.py | 53 ++++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/python-ecosys/requests/manifest.py b/python-ecosys/requests/manifest.py index 1571a29d9..9472bc103 100644 --- a/python-ecosys/requests/manifest.py +++ b/python-ecosys/requests/manifest.py @@ -1,3 +1,3 @@ -metadata(version="0.11.0", pypi="requests") +metadata(version="0.11.1", pypi="requests") package("requests") diff --git a/python-ecosys/requests/requests/__init__.py b/python-ecosys/requests/requests/__init__.py index deb262cf5..7f5672c52 100644 --- a/python-ecosys/requests/requests/__init__.py +++ b/python-ecosys/requests/requests/__init__.py @@ -172,6 +172,8 @@ def request( elif l.startswith(b"Location:") and not 200 <= status <= 299: if status in [301, 302, 303, 307, 308]: redirect = str(l[10:-2], "utf-8") + if redirect.startswith("/"): + redirect = proto + "//" + host + ":" + str(port) + redirect else: raise NotImplementedError("Redirect %d not yet supported" % status) if parse_headers is False: diff --git a/python-ecosys/requests/test_requests.py b/python-ecosys/requests/test_requests.py index ac77291b0..75c8c0982 100644 --- a/python-ecosys/requests/test_requests.py +++ b/python-ecosys/requests/test_requests.py @@ -5,7 +5,10 @@ class Socket: def __init__(self): self._write_buffer = io.BytesIO() - self._read_buffer = io.BytesIO(b"HTTP/1.0 200 OK\r\n\r\n") + self._read_buffer = io.BytesIO(Socket.RESPONSES.pop(0)) + + def close(self): + pass def connect(self, address): pass @@ -34,12 +37,20 @@ def socket(af=AF_INET, type=SOCK_STREAM, proto=IPPROTO_TCP): # ruff: noqa: E402 import requests +SERVER_RESPONSE_200_OK = b"HTTP/1.0 200 OK\r\n\r\n" + + +def set_server_responses(*buffers): + Socket.RESPONSES = list(buffers) + def format_message(response): return response.raw._write_buffer.getvalue().decode("utf8") def test_simple_get(): + set_server_responses(SERVER_RESPONSE_200_OK) + response = requests.request("GET", "http://example.com") assert response.raw._write_buffer.getvalue() == ( @@ -48,6 +59,8 @@ def test_simple_get(): def test_get_auth(): + set_server_responses(SERVER_RESPONSE_200_OK) + response = requests.request( "GET", "http://example.com", auth=("test-username", "test-password") ) @@ -61,6 +74,8 @@ def test_get_auth(): def test_get_custom_header(): + set_server_responses(SERVER_RESPONSE_200_OK) + response = requests.request("GET", "http://example.com", headers={"User-Agent": "test-agent"}) assert response.raw._write_buffer.getvalue() == ( @@ -72,6 +87,8 @@ def test_get_custom_header(): def test_post_json(): + set_server_responses(SERVER_RESPONSE_200_OK) + response = requests.request("GET", "http://example.com", json="test") assert response.raw._write_buffer.getvalue() == ( @@ -85,6 +102,8 @@ def test_post_json(): def test_post_chunked_data(): + set_server_responses(SERVER_RESPONSE_200_OK) + def chunks(): yield "test" @@ -101,6 +120,8 @@ def chunks(): def test_overwrite_get_headers(): + set_server_responses(SERVER_RESPONSE_200_OK) + response = requests.request( "GET", "http://example.com", headers={"Host": "test.com", "Connection": "keep-alive"} ) @@ -111,6 +132,8 @@ def test_overwrite_get_headers(): def test_overwrite_post_json_headers(): + set_server_responses(SERVER_RESPONSE_200_OK) + response = requests.request( "GET", "http://example.com", @@ -129,6 +152,8 @@ def test_overwrite_post_json_headers(): def test_overwrite_post_chunked_data_headers(): + set_server_responses(SERVER_RESPONSE_200_OK) + def chunks(): yield "test" @@ -146,6 +171,8 @@ def chunks(): def test_do_not_modify_headers_argument(): + set_server_responses(SERVER_RESPONSE_200_OK) + global do_not_modify_this_dict do_not_modify_this_dict = {} requests.request("GET", "http://example.com", headers=do_not_modify_this_dict) @@ -153,6 +180,28 @@ def test_do_not_modify_headers_argument(): assert do_not_modify_this_dict == {}, do_not_modify_this_dict +def test_redirect_with_protocol(): + set_server_responses( + b"HTTP/1.0 301 OK\r\nLocation: http://example.com/index\r\n\r\n", SERVER_RESPONSE_200_OK + ) + + response = requests.request("GET", "http://example.com") + + assert response.raw._write_buffer.getvalue() == ( + b"GET /index HTTP/1.0\r\n" + b"Connection: close\r\n" + b"Host: example.com\r\n\r\n" + ), format_message(response) + + +def test_redirect_without_protocol(): + set_server_responses(b"HTTP/1.0 301 OK\r\nLocation: /index\r\n\r\n", SERVER_RESPONSE_200_OK) + + response = requests.request("GET", "http://example.com") + + assert response.raw._write_buffer.getvalue() == ( + b"GET /index HTTP/1.0\r\n" + b"Connection: close\r\n" + b"Host: example.com\r\n\r\n" + ), format_message(response) + + test_simple_get() test_get_auth() test_get_custom_header() @@ -162,3 +211,5 @@ def test_do_not_modify_headers_argument(): test_overwrite_post_json_headers() test_overwrite_post_chunked_data_headers() test_do_not_modify_headers_argument() +test_redirect_with_protocol() +test_redirect_without_protocol()