Skip to content

Commit 9fe6096

Browse files
Harden transport request context normalization fallback boundaries
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 5822106 commit 9fe6096

2 files changed

Lines changed: 172 additions & 22 deletions

File tree

hyperbrowser/transport/error_utils.py

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,17 @@ def _safe_to_string(value: Any) -> str:
5151
normalized_value = str(value)
5252
except Exception:
5353
return f"<unstringifiable {type(value).__name__}>"
54-
sanitized_value = "".join(
55-
"?" if ord(character) < 32 or ord(character) == 127 else character
56-
for character in normalized_value
57-
)
58-
if sanitized_value.strip():
59-
return sanitized_value
54+
if not isinstance(normalized_value, str):
55+
return f"<{type(value).__name__}>"
56+
try:
57+
sanitized_value = "".join(
58+
"?" if ord(character) < 32 or ord(character) == 127 else character
59+
for character in normalized_value
60+
)
61+
if sanitized_value.strip():
62+
return sanitized_value
63+
except Exception:
64+
return f"<{type(value).__name__}>"
6065
return f"<{type(value).__name__}>"
6166

6267

@@ -83,18 +88,31 @@ def _normalize_request_method(method: Any) -> str:
8388
raw_method = str(raw_method)
8489
except Exception:
8590
return "UNKNOWN"
86-
if not isinstance(raw_method, str) or not raw_method.strip():
91+
try:
92+
if not isinstance(raw_method, str):
93+
return "UNKNOWN"
94+
stripped_method = raw_method.strip()
95+
if not isinstance(stripped_method, str) or not stripped_method:
96+
return "UNKNOWN"
97+
normalized_method = stripped_method.upper()
98+
if not isinstance(normalized_method, str):
99+
return "UNKNOWN"
100+
lowered_method = normalized_method.lower()
101+
if not isinstance(lowered_method, str):
102+
return "UNKNOWN"
103+
except Exception:
87104
return "UNKNOWN"
88-
normalized_method = raw_method.strip().upper()
89-
lowered_method = normalized_method.lower()
90105
if (
91106
lowered_method in _INVALID_METHOD_SENTINELS
92107
or _NUMERIC_LIKE_METHOD_PATTERN.fullmatch(normalized_method)
93108
):
94109
return "UNKNOWN"
95-
if len(normalized_method) > _MAX_REQUEST_METHOD_LENGTH:
96-
return "UNKNOWN"
97-
if not _HTTP_METHOD_TOKEN_PATTERN.fullmatch(normalized_method):
110+
try:
111+
if len(normalized_method) > _MAX_REQUEST_METHOD_LENGTH:
112+
return "UNKNOWN"
113+
if not _HTTP_METHOD_TOKEN_PATTERN.fullmatch(normalized_method):
114+
return "UNKNOWN"
115+
except Exception:
98116
return "UNKNOWN"
99117
return normalized_method
100118

@@ -116,22 +134,31 @@ def _normalize_request_url(url: Any) -> str:
116134
except Exception:
117135
return "unknown URL"
118136

119-
normalized_url = raw_url.strip()
120-
if not normalized_url:
137+
try:
138+
normalized_url = raw_url.strip()
139+
if not isinstance(normalized_url, str) or not normalized_url:
140+
return "unknown URL"
141+
lowered_url = normalized_url.lower()
142+
if not isinstance(lowered_url, str):
143+
return "unknown URL"
144+
except Exception:
121145
return "unknown URL"
122-
lowered_url = normalized_url.lower()
123146
if lowered_url in _INVALID_URL_SENTINELS or _NUMERIC_LIKE_URL_PATTERN.fullmatch(
124147
normalized_url
125148
):
126149
return "unknown URL"
127-
if any(character.isspace() for character in normalized_url):
128-
return "unknown URL"
129-
if any(
130-
ord(character) < 32 or ord(character) == 127 for character in normalized_url
131-
):
150+
try:
151+
if any(character.isspace() for character in normalized_url):
152+
return "unknown URL"
153+
if any(
154+
ord(character) < 32 or ord(character) == 127
155+
for character in normalized_url
156+
):
157+
return "unknown URL"
158+
if len(normalized_url) > _MAX_REQUEST_URL_DISPLAY_LENGTH:
159+
return f"{normalized_url[:_MAX_REQUEST_URL_DISPLAY_LENGTH]}... (truncated)"
160+
except Exception:
132161
return "unknown URL"
133-
if len(normalized_url) > _MAX_REQUEST_URL_DISPLAY_LENGTH:
134-
return f"{normalized_url[:_MAX_REQUEST_URL_DISPLAY_LENGTH]}... (truncated)"
135162
return normalized_url
136163

137164

tests/test_transport_error_utils.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,66 @@ def __str__(self) -> str:
205205
return "broken-slice-error-list"
206206

207207

208+
class _BrokenStripMethod(str):
209+
def strip(self, chars=None): # type: ignore[override]
210+
_ = chars
211+
raise RuntimeError("method strip exploded")
212+
213+
214+
class _BrokenUpperMethod(str):
215+
def strip(self, chars=None): # type: ignore[override]
216+
_ = chars
217+
return self
218+
219+
def upper(self): # type: ignore[override]
220+
raise RuntimeError("method upper exploded")
221+
222+
223+
class _BrokenMethodLength(str):
224+
def strip(self, chars=None): # type: ignore[override]
225+
_ = chars
226+
return self
227+
228+
def __len__(self):
229+
raise RuntimeError("method length exploded")
230+
231+
232+
class _BrokenStripUrl(str):
233+
def strip(self, chars=None): # type: ignore[override]
234+
_ = chars
235+
raise RuntimeError("url strip exploded")
236+
237+
238+
class _BrokenLowerUrl(str):
239+
def strip(self, chars=None): # type: ignore[override]
240+
_ = chars
241+
return self
242+
243+
def lower(self): # type: ignore[override]
244+
raise RuntimeError("url lower exploded")
245+
246+
247+
class _BrokenUrlIteration(str):
248+
def strip(self, chars=None): # type: ignore[override]
249+
_ = chars
250+
return self
251+
252+
def lower(self): # type: ignore[override]
253+
return "https://example.com/path"
254+
255+
def __iter__(self):
256+
raise RuntimeError("url iteration exploded")
257+
258+
259+
class _StringifiesToBrokenSubclass:
260+
class _BrokenString(str):
261+
def __iter__(self):
262+
raise RuntimeError("fallback string iteration exploded")
263+
264+
def __str__(self) -> str:
265+
return self._BrokenString("broken\tfallback\nvalue")
266+
267+
208268
def test_extract_request_error_context_uses_unknown_when_request_unset():
209269
method, url = extract_request_error_context(httpx.RequestError("network down"))
210270

@@ -663,6 +723,60 @@ def test_format_generic_request_failure_message_supports_memoryview_method_value
663723
assert message == "Request PATCH https://example.com/path failed"
664724

665725

726+
def test_format_generic_request_failure_message_normalizes_method_strip_failures():
727+
message = format_generic_request_failure_message(
728+
method=_BrokenStripMethod("get"),
729+
url="https://example.com/path",
730+
)
731+
732+
assert message == "Request UNKNOWN https://example.com/path failed"
733+
734+
735+
def test_format_generic_request_failure_message_normalizes_method_upper_failures():
736+
message = format_generic_request_failure_message(
737+
method=_BrokenUpperMethod("get"),
738+
url="https://example.com/path",
739+
)
740+
741+
assert message == "Request UNKNOWN https://example.com/path failed"
742+
743+
744+
def test_format_generic_request_failure_message_normalizes_method_length_failures():
745+
message = format_generic_request_failure_message(
746+
method=_BrokenMethodLength("get"),
747+
url="https://example.com/path",
748+
)
749+
750+
assert message == "Request UNKNOWN https://example.com/path failed"
751+
752+
753+
def test_format_generic_request_failure_message_normalizes_url_strip_failures():
754+
message = format_generic_request_failure_message(
755+
method="GET",
756+
url=_BrokenStripUrl("https://example.com/path"),
757+
)
758+
759+
assert message == "Request GET unknown URL failed"
760+
761+
762+
def test_format_generic_request_failure_message_normalizes_url_lower_failures():
763+
message = format_generic_request_failure_message(
764+
method="GET",
765+
url=_BrokenLowerUrl("https://example.com/path"),
766+
)
767+
768+
assert message == "Request GET unknown URL failed"
769+
770+
771+
def test_format_generic_request_failure_message_normalizes_url_iteration_failures():
772+
message = format_generic_request_failure_message(
773+
method="GET",
774+
url=_BrokenUrlIteration("https://example.com/path"),
775+
)
776+
777+
assert message == "Request GET unknown URL failed"
778+
779+
666780
def test_format_request_failure_message_truncates_very_long_fallback_urls():
667781
very_long_url = "https://example.com/" + ("a" * 1200)
668782
message = format_request_failure_message(
@@ -801,6 +915,15 @@ def test_extract_error_message_sanitizes_control_characters_in_fallback_error_te
801915
assert message == "bad?fallback?text"
802916

803917

918+
def test_extract_error_message_handles_fallback_errors_with_broken_string_subclasses():
919+
message = extract_error_message(
920+
_DummyResponse(" ", text=" "),
921+
_StringifiesToBrokenSubclass(),
922+
)
923+
924+
assert message == "<_StringifiesToBrokenSubclass>"
925+
926+
804927
def test_extract_error_message_sanitizes_control_characters_in_json_message():
805928
message = extract_error_message(
806929
_DummyResponse({"message": "bad\tjson\nmessage"}),

0 commit comments

Comments
 (0)