Skip to content

Commit 260d09e

Browse files
Centralize base URL normalization and validation
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent eed6d2a commit 260d09e

File tree

3 files changed

+38
-27
lines changed

3 files changed

+38
-27
lines changed

hyperbrowser/client/base.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -87,24 +87,8 @@ def _build_url(self, path: str) -> str:
8787
normalized_query_suffix = (
8888
f"?{normalized_parts.query}" if normalized_parts.query else ""
8989
)
90-
if not isinstance(self.config.base_url, str):
91-
raise HyperbrowserError("base_url must be a string")
92-
normalized_base_url = self.config.base_url.strip().rstrip("/")
93-
if not normalized_base_url:
94-
raise HyperbrowserError("base_url must not be empty")
95-
90+
normalized_base_url = ClientConfig.normalize_base_url(self.config.base_url)
9691
parsed_base_url = urlparse(normalized_base_url)
97-
if (
98-
parsed_base_url.scheme not in {"https", "http"}
99-
or not parsed_base_url.netloc
100-
):
101-
raise HyperbrowserError(
102-
"base_url must start with 'https://' or 'http://' and include a host"
103-
)
104-
if parsed_base_url.query or parsed_base_url.fragment:
105-
raise HyperbrowserError(
106-
"base_url must not include query parameters or fragments"
107-
)
10892
base_has_api_suffix = parsed_base_url.path.rstrip("/").endswith("/api")
10993

11094
if normalized_path_only == "/api" or normalized_path_only.startswith("/api/"):

hyperbrowser/config.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,24 @@ class ClientConfig:
1818
def __post_init__(self) -> None:
1919
if not isinstance(self.api_key, str):
2020
raise HyperbrowserError("api_key must be a string")
21-
if not isinstance(self.base_url, str):
22-
raise HyperbrowserError("base_url must be a string")
2321
self.api_key = self.api_key.strip()
2422
if not self.api_key:
2523
raise HyperbrowserError("api_key must not be empty")
26-
self.base_url = self.base_url.strip().rstrip("/")
27-
if not self.base_url:
24+
self.base_url = self.normalize_base_url(self.base_url)
25+
self.headers = normalize_headers(
26+
self.headers,
27+
mapping_error_message="headers must be a mapping of string pairs",
28+
)
29+
30+
@staticmethod
31+
def normalize_base_url(base_url: str) -> str:
32+
if not isinstance(base_url, str):
33+
raise HyperbrowserError("base_url must be a string")
34+
normalized_base_url = base_url.strip().rstrip("/")
35+
if not normalized_base_url:
2836
raise HyperbrowserError("base_url must not be empty")
29-
parsed_base_url = urlparse(self.base_url)
37+
38+
parsed_base_url = urlparse(normalized_base_url)
3039
if (
3140
parsed_base_url.scheme not in {"https", "http"}
3241
or not parsed_base_url.netloc
@@ -38,10 +47,7 @@ def __post_init__(self) -> None:
3847
raise HyperbrowserError(
3948
"base_url must not include query parameters or fragments"
4049
)
41-
self.headers = normalize_headers(
42-
self.headers,
43-
mapping_error_message="headers must be a mapping of string pairs",
44-
)
50+
return normalized_base_url
4551

4652
@classmethod
4753
def from_env(cls) -> "ClientConfig":
@@ -67,4 +73,4 @@ def resolve_base_url_from_env(raw_base_url: Optional[str]) -> str:
6773
return "https://api.hyperbrowser.ai"
6874
if not raw_base_url.strip():
6975
raise HyperbrowserError("HYPERBROWSER_BASE_URL must not be empty when set")
70-
return raw_base_url
76+
return ClientConfig.normalize_base_url(raw_base_url)

tests/test_config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,24 @@ def test_client_config_resolve_base_url_from_env_defaults_and_rejects_blank():
288288
HyperbrowserError, match="HYPERBROWSER_BASE_URL must not be empty"
289289
):
290290
ClientConfig.resolve_base_url_from_env(" ")
291+
with pytest.raises(HyperbrowserError, match="include a host"):
292+
ClientConfig.resolve_base_url_from_env("https://")
293+
294+
295+
def test_client_config_normalize_base_url_validates_and_normalizes():
296+
assert (
297+
ClientConfig.normalize_base_url(" https://example.local/custom/api/ ")
298+
== "https://example.local/custom/api"
299+
)
300+
301+
with pytest.raises(HyperbrowserError, match="base_url must be a string"):
302+
ClientConfig.normalize_base_url(None) # type: ignore[arg-type]
303+
304+
with pytest.raises(HyperbrowserError, match="base_url must not be empty"):
305+
ClientConfig.normalize_base_url(" ")
306+
307+
with pytest.raises(HyperbrowserError, match="base_url must start with"):
308+
ClientConfig.normalize_base_url("example.local")
309+
310+
with pytest.raises(HyperbrowserError, match="must not include query parameters"):
311+
ClientConfig.normalize_base_url("https://example.local?foo=bar")

0 commit comments

Comments
 (0)