Skip to content

Commit f89ea1f

Browse files
Harden HyperbrowserBase env reads and path normalization
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 740aeb6 commit f89ea1f

File tree

3 files changed

+164
-4
lines changed

3 files changed

+164
-4
lines changed

hyperbrowser/client/base.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def __init__(
3030
resolved_api_key = (
3131
api_key
3232
if api_key_from_constructor
33-
else os.environ.get("HYPERBROWSER_API_KEY")
33+
else self._read_env_value("HYPERBROWSER_API_KEY")
3434
)
3535
if resolved_api_key is None:
3636
raise HyperbrowserError(
@@ -59,12 +59,12 @@ def __init__(
5959
headers
6060
if headers is not None
6161
else ClientConfig.parse_headers_from_env(
62-
os.environ.get("HYPERBROWSER_HEADERS")
62+
self._read_env_value("HYPERBROWSER_HEADERS")
6363
)
6464
)
6565
if base_url is None:
6666
resolved_base_url = ClientConfig.resolve_base_url_from_env(
67-
os.environ.get("HYPERBROWSER_BASE_URL")
67+
self._read_env_value("HYPERBROWSER_BASE_URL")
6868
)
6969
else:
7070
resolved_base_url = base_url
@@ -80,6 +80,18 @@ def __init__(
8080
self.config = config
8181
self.transport = transport(config.api_key, headers=config.headers)
8282

83+
@staticmethod
84+
def _read_env_value(env_name: str) -> Optional[str]:
85+
try:
86+
return os.environ.get(env_name)
87+
except HyperbrowserError:
88+
raise
89+
except Exception as exc:
90+
raise HyperbrowserError(
91+
f"Failed to read {env_name} environment variable",
92+
original_error=exc,
93+
) from exc
94+
8395
@staticmethod
8496
def _parse_url_components(
8597
url_value: str, *, component_label: str
@@ -127,7 +139,17 @@ def _parse_url_components(
127139
def _build_url(self, path: str) -> str:
128140
if not isinstance(path, str):
129141
raise HyperbrowserError("path must be a string")
130-
stripped_path = path.strip()
142+
try:
143+
stripped_path = path.strip()
144+
if not isinstance(stripped_path, str):
145+
raise TypeError("normalized path must be a string")
146+
except HyperbrowserError:
147+
raise
148+
except Exception as exc:
149+
raise HyperbrowserError(
150+
"Failed to normalize path",
151+
original_error=exc,
152+
) from exc
131153
if stripped_path != path:
132154
raise HyperbrowserError(
133155
"path must not contain leading or trailing whitespace"

tests/test_client_api_key.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22

3+
import hyperbrowser.client.base as client_base_module
34
from hyperbrowser import AsyncHyperbrowser, Hyperbrowser
45
from hyperbrowser.exceptions import HyperbrowserError
56

@@ -110,6 +111,95 @@ def test_async_client_rejects_control_character_env_api_key(monkeypatch):
110111
AsyncHyperbrowser()
111112

112113

114+
@pytest.mark.parametrize("client_class", [Hyperbrowser, AsyncHyperbrowser])
115+
def test_client_wraps_api_key_env_read_runtime_errors(
116+
client_class, monkeypatch: pytest.MonkeyPatch
117+
):
118+
original_get = client_base_module.os.environ.get
119+
120+
def _broken_get(env_name: str, default=None):
121+
if env_name == "HYPERBROWSER_API_KEY":
122+
raise RuntimeError("api key env read exploded")
123+
return original_get(env_name, default)
124+
125+
monkeypatch.setattr(client_base_module.os.environ, "get", _broken_get)
126+
127+
with pytest.raises(
128+
HyperbrowserError,
129+
match="Failed to read HYPERBROWSER_API_KEY environment variable",
130+
) as exc_info:
131+
client_class()
132+
133+
assert isinstance(exc_info.value.original_error, RuntimeError)
134+
135+
136+
@pytest.mark.parametrize("client_class", [Hyperbrowser, AsyncHyperbrowser])
137+
def test_client_preserves_hyperbrowser_api_key_env_read_errors(
138+
client_class, monkeypatch: pytest.MonkeyPatch
139+
):
140+
original_get = client_base_module.os.environ.get
141+
142+
def _broken_get(env_name: str, default=None):
143+
if env_name == "HYPERBROWSER_API_KEY":
144+
raise HyperbrowserError("custom api key env read failure")
145+
return original_get(env_name, default)
146+
147+
monkeypatch.setattr(client_base_module.os.environ, "get", _broken_get)
148+
149+
with pytest.raises(
150+
HyperbrowserError, match="custom api key env read failure"
151+
) as exc_info:
152+
client_class()
153+
154+
assert exc_info.value.original_error is None
155+
156+
157+
@pytest.mark.parametrize("client_class", [Hyperbrowser, AsyncHyperbrowser])
158+
def test_client_wraps_base_url_env_read_runtime_errors(
159+
client_class, monkeypatch: pytest.MonkeyPatch
160+
):
161+
original_get = client_base_module.os.environ.get
162+
monkeypatch.setenv("HYPERBROWSER_API_KEY", "test-key")
163+
164+
def _broken_get(env_name: str, default=None):
165+
if env_name == "HYPERBROWSER_BASE_URL":
166+
raise RuntimeError("base url env read exploded")
167+
return original_get(env_name, default)
168+
169+
monkeypatch.setattr(client_base_module.os.environ, "get", _broken_get)
170+
171+
with pytest.raises(
172+
HyperbrowserError,
173+
match="Failed to read HYPERBROWSER_BASE_URL environment variable",
174+
) as exc_info:
175+
client_class()
176+
177+
assert isinstance(exc_info.value.original_error, RuntimeError)
178+
179+
180+
@pytest.mark.parametrize("client_class", [Hyperbrowser, AsyncHyperbrowser])
181+
def test_client_wraps_headers_env_read_runtime_errors(
182+
client_class, monkeypatch: pytest.MonkeyPatch
183+
):
184+
original_get = client_base_module.os.environ.get
185+
monkeypatch.setenv("HYPERBROWSER_API_KEY", "test-key")
186+
187+
def _broken_get(env_name: str, default=None):
188+
if env_name == "HYPERBROWSER_HEADERS":
189+
raise RuntimeError("headers env read exploded")
190+
return original_get(env_name, default)
191+
192+
monkeypatch.setattr(client_base_module.os.environ, "get", _broken_get)
193+
194+
with pytest.raises(
195+
HyperbrowserError,
196+
match="Failed to read HYPERBROWSER_HEADERS environment variable",
197+
) as exc_info:
198+
client_class()
199+
200+
assert isinstance(exc_info.value.original_error, RuntimeError)
201+
202+
113203
@pytest.mark.parametrize("client_class", [Hyperbrowser, AsyncHyperbrowser])
114204
def test_client_wraps_api_key_strip_runtime_errors(client_class):
115205
class _BrokenStripApiKey(str):

tests/test_url_building.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,54 @@ def test_client_build_url_rejects_empty_or_non_string_paths():
353353
client.close()
354354

355355

356+
def test_client_build_url_wraps_path_strip_runtime_errors():
357+
client = Hyperbrowser(config=ClientConfig(api_key="test-key"))
358+
try:
359+
class _BrokenPath(str):
360+
def strip(self, chars=None): # type: ignore[override]
361+
_ = chars
362+
raise RuntimeError("path strip exploded")
363+
364+
with pytest.raises(HyperbrowserError, match="Failed to normalize path") as exc_info:
365+
client._build_url(_BrokenPath("/session"))
366+
367+
assert isinstance(exc_info.value.original_error, RuntimeError)
368+
finally:
369+
client.close()
370+
371+
372+
def test_client_build_url_preserves_hyperbrowser_path_strip_errors():
373+
client = Hyperbrowser(config=ClientConfig(api_key="test-key"))
374+
try:
375+
class _BrokenPath(str):
376+
def strip(self, chars=None): # type: ignore[override]
377+
_ = chars
378+
raise HyperbrowserError("custom path strip failure")
379+
380+
with pytest.raises(HyperbrowserError, match="custom path strip failure") as exc_info:
381+
client._build_url(_BrokenPath("/session"))
382+
383+
assert exc_info.value.original_error is None
384+
finally:
385+
client.close()
386+
387+
388+
def test_client_build_url_wraps_non_string_path_strip_results():
389+
client = Hyperbrowser(config=ClientConfig(api_key="test-key"))
390+
try:
391+
class _BrokenPath(str):
392+
def strip(self, chars=None): # type: ignore[override]
393+
_ = chars
394+
return object()
395+
396+
with pytest.raises(HyperbrowserError, match="Failed to normalize path") as exc_info:
397+
client._build_url(_BrokenPath("/session"))
398+
399+
assert isinstance(exc_info.value.original_error, TypeError)
400+
finally:
401+
client.close()
402+
403+
356404
def test_client_build_url_allows_query_values_containing_absolute_urls():
357405
client = Hyperbrowser(config=ClientConfig(api_key="test-key"))
358406
try:

0 commit comments

Comments
 (0)