From 3adcd50826f64be3a1c434003fe9ca870a596dac Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Thu, 22 Oct 2026 03:51:37 +0300 Subject: [PATCH 1/3] Respect the value of "allow_redirects" parameter in `_request` --- qfieldcloud_sdk/sdk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qfieldcloud_sdk/sdk.py b/qfieldcloud_sdk/sdk.py index f7330aa..a435f00 100644 --- a/qfieldcloud_sdk/sdk.py +++ b/qfieldcloud_sdk/sdk.py @@ -2131,7 +2131,8 @@ def _request( "GET requests must not have `data` values passed, use `params` instead" ) - allow_redirects = method != "POST" + if allow_redirects is None: + allow_redirects = method != "POST" if not skip_token and self.token: headers_copy["Authorization"] = f"token {self.token}" From e52ef22a322a3d4e1e19079fb500fbd13b054a14 Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Thu, 22 Oct 2026 03:53:19 +0300 Subject: [PATCH 2/3] Remove useless `else` block --- qfieldcloud_sdk/sdk.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/qfieldcloud_sdk/sdk.py b/qfieldcloud_sdk/sdk.py index a435f00..f2dcc5b 100644 --- a/qfieldcloud_sdk/sdk.py +++ b/qfieldcloud_sdk/sdk.py @@ -2176,24 +2176,24 @@ def _request( if os.environ.get("ENVIRONMENT") == "test": return request.mock_response() - else: - prepared_request = self.session.prepare_request(request) - - # Merge environment settings into session - settings = self.session.merge_environment_settings( - url=prepared_request.url, - proxies={}, - stream=stream, - verify=self.verify_ssl, - cert=None, - ) - settings = settings | session_params # type: ignore - response = self.session.send( - prepared_request, - timeout=(self.connect_timeout, self.read_timeout), - **settings, - ) + prepared_request = self.session.prepare_request(request) + + # Merge environment settings into session + settings = self.session.merge_environment_settings( + url=prepared_request.url, + proxies={}, + stream=stream, + verify=self.verify_ssl, + cert=None, + ) + settings = settings | session_params # type: ignore + + response = self.session.send( + prepared_request, + timeout=(self.connect_timeout, self.read_timeout), + **settings, + ) try: response.raise_for_status() From 2cad5920db4e7aa53eb5191b421db16ad8a0fb63 Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Thu, 22 Oct 2026 03:53:56 +0300 Subject: [PATCH 3/3] When we do not allow redirects and get one, raise an QfcRequestException --- qfieldcloud_sdk/sdk.py | 7 +++++++ tests/test_cli_client.py | 25 ++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/qfieldcloud_sdk/sdk.py b/qfieldcloud_sdk/sdk.py index f2dcc5b..d3c1f83 100644 --- a/qfieldcloud_sdk/sdk.py +++ b/qfieldcloud_sdk/sdk.py @@ -2195,6 +2195,13 @@ def _request( **settings, ) + if not allow_redirects and response.is_redirect: + raise QfcRequestException( + response, + f"Unresolved redirect for {method} {response.url!r}: " + f"{response.status_code} -> {response.headers.get('Location')!r}", + ) + try: response.raise_for_status() except Exception as err: diff --git a/tests/test_cli_client.py b/tests/test_cli_client.py index 4f98fad..0d8b40e 100644 --- a/tests/test_cli_client.py +++ b/tests/test_cli_client.py @@ -2,10 +2,12 @@ import unittest from unittest import mock +import requests from click.testing import CliRunner +from requests.structures import CaseInsensitiveDict from qfieldcloud_sdk.cli import QFIELDCLOUD_DEFAULT_URL, cli -from qfieldcloud_sdk.sdk import Client, Pagination, QfcException +from qfieldcloud_sdk.sdk import Client, Pagination, QfcException, QfcRequestException class TestSDK(unittest.TestCase): @@ -92,6 +94,27 @@ def test_timeout_invalid_arg_value_raises(self): with self.assertRaises(QfcException): Client(QFIELDCLOUD_DEFAULT_URL, read_timeout="soon") # type: ignore + def test_request_raises_on_unresolved_post_redirect(self): + fake_response = requests.Response() + fake_response.status_code = 301 + fake_response.reason = "Moved Permanently" + fake_response.url = f"{self.client.url}files/some-project/some-file.gpkg/" + fake_response.headers = CaseInsensitiveDict( + {"Location": "http://another-address/files/some-project/some-file.gpkg/"} + ) + fake_response._content = b"" + + # We need to unset the `ENVIRONMENT` variable, because otherwise the client + # would use the mock response and we cannot test the redirect behavior. + with mock.patch.dict(os.environ, {}, clear=False): + os.environ.pop("ENVIRONMENT", None) + + with mock.patch.object( + self.client.session, "send", return_value=fake_response + ): + with self.assertRaises(QfcRequestException): + self.client._request("POST", "files/some-project/some-file.gpkg") + class TestCLI(unittest.TestCase): @classmethod