@@ -17,6 +17,7 @@ | |||
| 17 | 17 | """Wrapper for the GitLab API.""" | |
| 18 | 18 | ||
| 19 | 19 | import os | |
| 20 | + import re | ||
| 20 | 21 | import time | |
| 21 | 22 | from typing import Any, cast, Dict, List, Optional, Tuple, TYPE_CHECKING, Union | |
| 22 | 23 | ||
@@ -65,6 +66,8 @@ class Gitlab: | |||
| 65 | 66 | user_agent: A custom user agent to use for making HTTP requests. | |
| 66 | 67 | retry_transient_errors: Whether to retry after 500, 502, 503, 504 | |
| 67 | 68 | or 52x responses. Defaults to False. | |
| 69 | + keep_base_url: keep user-provided base URL for pagination if it | ||
| 70 | + differs from response headers | ||
| 68 | 71 | """ | |
| 69 | 72 | ||
| 70 | 73 | def __init__( | |
@@ -84,6 +87,7 @@ def __init__( | |||
| 84 | 87 | order_by: Optional[str] = None, | |
| 85 | 88 | user_agent: str = gitlab.const.USER_AGENT, | |
| 86 | 89 | retry_transient_errors: bool = False, | |
| 90 | + keep_base_url: bool = False, | ||
| 87 | 91 | ) -> None: | |
| 88 | 92 | ||
| 89 | 93 | self._api_version = str(api_version) | |
@@ -94,6 +98,7 @@ def __init__( | |||
| 94 | 98 | #: Timeout to use for requests to gitlab server | |
| 95 | 99 | self.timeout = timeout | |
| 96 | 100 | self.retry_transient_errors = retry_transient_errors | |
| 101 | + self.keep_base_url = keep_base_url | ||
| 97 | 102 | #: Headers that will be used in request to GitLab | |
| 98 | 103 | self.headers = {"User-Agent": user_agent} | |
| 99 | 104 | ||
@@ -1132,6 +1137,30 @@ def _query( | |||
| 1132 | 1137 | next_url = requests.utils.parse_header_links(result.headers["links"])[ | |
| 1133 | 1138 | 0 | |
| 1134 | 1139 | ]["url"] | |
| 1140 | + # if the next url is different with user provided server URL | ||
| 1141 | + # then give a warning it may because of misconfiguration | ||
| 1142 | + # but if the option to fix provided then just reconstruct it | ||
| 1143 | + if not next_url.startswith(self._gl.url): | ||
| 1144 | + search_api_url = re.search(r"(^.*?/api)", next_url) | ||
| 1145 | + if search_api_url: | ||
| 1146 | + next_api_url = search_api_url.group(1) | ||
| 1147 | + if self._gl.keep_base_url: | ||
| 1148 | + next_url = next_url.replace( | ||
| 1149 | + next_api_url, f"{self._gl._base_url}/api" | ||
| 1150 | + ) | ||
| 1151 | + else: | ||
| 1152 | + utils.warn( | ||
| 1153 | + message=( | ||
| 1154 | + f"The base URL in the server response" | ||
| 1155 | + f"differs from the user-provided base URL " | ||
| 1156 | + f"({self._gl.url}/api/ -> {next_api_url}/). " | ||
| 1157 | + f"This may lead to unexpected behavior and " | ||
| 1158 | + f"broken pagination. Use `keep_base_url=True` " | ||
| 1159 | + f"when initializing the Gitlab instance " | ||
| 1160 | + f"to follow the user-provided base URL." | ||
| 1161 | + ), | ||
| 1162 | + category=UserWarning, | ||
| 1163 | + ) | ||
| 1135 | 1164 | self._next_url = next_url | |
| 1136 | 1165 | except KeyError: | |
| 1137 | 1166 | self._next_url = None | |
@@ -353,3 +353,58 @@ def test_gitlab_plain_const_does_not_warn(recwarn): | |||
| 353 | 353 | ||
| 354 | 354 | assert not recwarn | |
| 355 | 355 | assert no_access == 0 | |
| 356 | + | ||
| 357 | + | ||
| 358 | + @responses.activate | ||
| 359 | + @pytest.mark.parametrize( | ||
| 360 | + "kwargs,link_header,expected_next_url,show_warning", | ||
| 361 | + [ | ||
| 362 | + ( | ||
| 363 | + {}, | ||
| 364 | + "<http://localhost/api/v4/tests?per_page=1&page=2>;" ' rel="next"', | ||
| 365 | + "http://localhost/api/v4/tests?per_page=1&page=2", | ||
| 366 | + False, | ||
| 367 | + ), | ||
| 368 | + ( | ||
| 369 | + {}, | ||
| 370 | + "<http://orig_host/api/v4/tests?per_page=1&page=2>;" ' rel="next"', | ||
| 371 | + "http://orig_host/api/v4/tests?per_page=1&page=2", | ||
| 372 | + True, | ||
| 373 | + ), | ||
| 374 | + ( | ||
| 375 | + {"keep_base_url": True}, | ||
| 376 | + "<http://orig_host/api/v4/tests?per_page=1&page=2>;" ' rel="next"', | ||
| 377 | + "http://localhost/api/v4/tests?per_page=1&page=2", | ||
| 378 | + False, | ||
| 379 | + ), | ||
| 380 | + ], | ||
| 381 | + ids=["url-match-does-not-warn", "url-mismatch-warns", "url-mismatch-keeps-url"], | ||
| 382 | + ) | ||
| 383 | + def test_gitlab_keep_base_url(kwargs, link_header, expected_next_url, show_warning): | ||
| 384 | + responses.add( | ||
| 385 | + **{ | ||
| 386 | + "method": responses.GET, | ||
| 387 | + "url": "http://localhost/api/v4/tests", | ||
| 388 | + "json": [{"a": "b"}], | ||
| 389 | + "headers": { | ||
| 390 | + "X-Page": "1", | ||
| 391 | + "X-Next-Page": "2", | ||
| 392 | + "X-Per-Page": "1", | ||
| 393 | + "X-Total-Pages": "2", | ||
| 394 | + "X-Total": "2", | ||
| 395 | + "Link": (link_header), | ||
| 396 | + }, | ||
| 397 | + "content_type": "application/json", | ||
| 398 | + "status": 200, | ||
| 399 | + "match": helpers.MATCH_EMPTY_QUERY_PARAMS, | ||
| 400 | + } | ||
| 401 | + ) | ||
| 402 | + | ||
| 403 | + gl = gitlab.Gitlab(url="http://localhost", **kwargs) | ||
| 404 | + if show_warning: | ||
| 405 | + with pytest.warns(UserWarning) as warn_record: | ||
| 406 | + obj = gl.http_list("/tests", iterator=True) | ||
| 407 | + assert len(warn_record) == 1 | ||
| 408 | + else: | ||
| 409 | + obj = gl.http_list("/tests", iterator=True) | ||
| 410 | + assert obj._next_url == expected_next_url | ||
0 commit comments