Skip to content

Commit da6693e

Browse files
Guard base URL host and credential parsing errors
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 724ae14 commit da6693e

File tree

2 files changed

+114
-5
lines changed

2 files changed

+114
-5
lines changed

hyperbrowser/config.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,17 @@ def normalize_base_url(base_url: str) -> str:
108108
or not isinstance(parsed_base_url.fragment, str)
109109
):
110110
raise HyperbrowserError("base_url parser returned invalid URL components")
111-
if (
112-
parsed_base_url.hostname is not None
113-
and not isinstance(parsed_base_url.hostname, str)
111+
try:
112+
parsed_base_url_hostname = parsed_base_url.hostname
113+
except HyperbrowserError:
114+
raise
115+
except Exception as exc:
116+
raise HyperbrowserError(
117+
"Failed to parse base_url host",
118+
original_error=exc,
119+
) from exc
120+
if parsed_base_url_hostname is not None and not isinstance(
121+
parsed_base_url_hostname, str
114122
):
115123
raise HyperbrowserError("base_url parser returned invalid URL components")
116124
if (
@@ -120,15 +128,25 @@ def normalize_base_url(base_url: str) -> str:
120128
raise HyperbrowserError(
121129
"base_url must start with 'https://' or 'http://' and include a host"
122130
)
123-
if parsed_base_url.hostname is None:
131+
if parsed_base_url_hostname is None:
124132
raise HyperbrowserError(
125133
"base_url must start with 'https://' or 'http://' and include a host"
126134
)
127135
if parsed_base_url.query or parsed_base_url.fragment:
128136
raise HyperbrowserError(
129137
"base_url must not include query parameters or fragments"
130138
)
131-
if parsed_base_url.username is not None or parsed_base_url.password is not None:
139+
try:
140+
parsed_base_url_username = parsed_base_url.username
141+
parsed_base_url_password = parsed_base_url.password
142+
except HyperbrowserError:
143+
raise
144+
except Exception as exc:
145+
raise HyperbrowserError(
146+
"Failed to parse base_url credentials",
147+
original_error=exc,
148+
) from exc
149+
if parsed_base_url_username is not None or parsed_base_url_password is not None:
132150
raise HyperbrowserError("base_url must not include user credentials")
133151
try:
134152
parsed_base_url.port

tests/test_config.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,97 @@ def port(self) -> int:
509509
ClientConfig.normalize_base_url("https://example.local")
510510

511511

512+
def test_client_config_normalize_base_url_wraps_hostname_access_errors(
513+
monkeypatch: pytest.MonkeyPatch,
514+
):
515+
class _ParsedURL:
516+
scheme = "https"
517+
netloc = "example.local"
518+
query = ""
519+
fragment = ""
520+
username = None
521+
password = None
522+
path = "/api"
523+
524+
@property
525+
def hostname(self):
526+
raise RuntimeError("hostname parser exploded")
527+
528+
@property
529+
def port(self) -> int:
530+
return 443
531+
532+
monkeypatch.setattr(config_module, "urlparse", lambda _value: _ParsedURL())
533+
534+
with pytest.raises(HyperbrowserError, match="Failed to parse base_url host") as exc_info:
535+
ClientConfig.normalize_base_url("https://example.local")
536+
537+
assert exc_info.value.original_error is not None
538+
539+
540+
def test_client_config_normalize_base_url_wraps_credential_access_errors(
541+
monkeypatch: pytest.MonkeyPatch,
542+
):
543+
class _ParsedURL:
544+
scheme = "https"
545+
netloc = "example.local"
546+
hostname = "example.local"
547+
query = ""
548+
fragment = ""
549+
path = "/api"
550+
551+
@property
552+
def username(self):
553+
raise RuntimeError("credential parser exploded")
554+
555+
@property
556+
def password(self):
557+
return None
558+
559+
@property
560+
def port(self) -> int:
561+
return 443
562+
563+
monkeypatch.setattr(config_module, "urlparse", lambda _value: _ParsedURL())
564+
565+
with pytest.raises(
566+
HyperbrowserError, match="Failed to parse base_url credentials"
567+
) as exc_info:
568+
ClientConfig.normalize_base_url("https://example.local")
569+
570+
assert exc_info.value.original_error is not None
571+
572+
573+
def test_client_config_normalize_base_url_preserves_hyperbrowser_hostname_errors(
574+
monkeypatch: pytest.MonkeyPatch,
575+
):
576+
class _ParsedURL:
577+
scheme = "https"
578+
netloc = "example.local"
579+
query = ""
580+
fragment = ""
581+
username = None
582+
password = None
583+
path = "/api"
584+
585+
@property
586+
def hostname(self):
587+
raise HyperbrowserError("custom hostname parser failure")
588+
589+
@property
590+
def port(self) -> int:
591+
return 443
592+
593+
monkeypatch.setattr(config_module, "urlparse", lambda _value: _ParsedURL())
594+
595+
with pytest.raises(
596+
HyperbrowserError, match="custom hostname parser failure"
597+
) as exc_info:
598+
ClientConfig.normalize_base_url("https://example.local")
599+
600+
assert exc_info.value.original_error is None
601+
602+
512603
def test_client_config_normalize_base_url_wraps_path_decode_runtime_errors(
513604
monkeypatch: pytest.MonkeyPatch,
514605
):

0 commit comments

Comments
 (0)