Skip to content

Commit 05ba2dd

Browse files
Harden unreadable key display fallbacks across parsers and tools
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 844fb89 commit 05ba2dd

6 files changed

Lines changed: 122 additions & 13 deletions

File tree

hyperbrowser/client/managers/extension_utils.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,18 @@ def _safe_stringify_key(value: object) -> str:
2121

2222

2323
def _format_key_display(value: object) -> str:
24-
normalized_key = _safe_stringify_key(value)
25-
normalized_key = "".join(
26-
"?" if ord(character) < 32 or ord(character) == 127 else character
27-
for character in normalized_key
28-
).strip()
24+
try:
25+
normalized_key = _safe_stringify_key(value)
26+
if not isinstance(normalized_key, str):
27+
raise TypeError("normalized key display must be a string")
28+
normalized_key = "".join(
29+
"?" if ord(character) < 32 or ord(character) == 127 else character
30+
for character in normalized_key
31+
).strip()
32+
if not isinstance(normalized_key, str):
33+
raise TypeError("normalized key display must be a string")
34+
except Exception:
35+
return "<unreadable key>"
2936
if not normalized_key:
3037
return "<blank key>"
3138
if len(normalized_key) <= _MAX_DISPLAYED_MISSING_KEY_LENGTH:

hyperbrowser/client/managers/session_utils.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@
1111

1212

1313
def _format_recording_key_display(key: str) -> str:
14-
normalized_key = "".join(
15-
"?" if ord(character) < 32 or ord(character) == 127 else character
16-
for character in key
17-
).strip()
14+
try:
15+
normalized_key = "".join(
16+
"?" if ord(character) < 32 or ord(character) == 127 else character
17+
for character in key
18+
).strip()
19+
if not isinstance(normalized_key, str):
20+
raise TypeError("normalized recording key display must be a string")
21+
except Exception:
22+
return "<unreadable key>"
1823
if not normalized_key:
1924
return "<blank key>"
2025
if len(normalized_key) <= _MAX_KEY_DISPLAY_LENGTH:

hyperbrowser/tools/__init__.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,15 @@ def _has_declared_attribute(
5353

5454

5555
def _format_tool_param_key_for_error(key: str) -> str:
56-
normalized_key = "".join(
57-
"?" if ord(character) < 32 or ord(character) == 127 else character
58-
for character in key
59-
).strip()
56+
try:
57+
normalized_key = "".join(
58+
"?" if ord(character) < 32 or ord(character) == 127 else character
59+
for character in key
60+
).strip()
61+
if not isinstance(normalized_key, str):
62+
raise TypeError("normalized tool key display must be a string")
63+
except Exception:
64+
return "<unreadable key>"
6065
if not normalized_key:
6166
return "<blank key>"
6267
if len(normalized_key) <= _MAX_KEY_DISPLAY_LENGTH:

tests/test_extension_utils.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,37 @@ def __getitem__(self, key: str) -> object:
349349
assert exc_info.value.original_error is not None
350350

351351

352+
def test_parse_extension_list_response_data_falls_back_for_unreadable_value_read_keys():
353+
class _BrokenKey(str):
354+
class _BrokenRenderedKey(str):
355+
def __iter__(self):
356+
raise RuntimeError("cannot iterate rendered extension key")
357+
358+
def __str__(self) -> str:
359+
return self._BrokenRenderedKey("name")
360+
361+
class _BrokenValueLookupMapping(Mapping[str, object]):
362+
def __iter__(self) -> Iterator[str]:
363+
yield _BrokenKey("name")
364+
365+
def __len__(self) -> int:
366+
return 1
367+
368+
def __getitem__(self, key: str) -> object:
369+
_ = key
370+
raise RuntimeError("cannot read extension value")
371+
372+
with pytest.raises(
373+
HyperbrowserError,
374+
match="Failed to read extension object value for key '<unreadable key>' at index 0",
375+
) as exc_info:
376+
parse_extension_list_response_data(
377+
{"extensions": [_BrokenValueLookupMapping()]}
378+
)
379+
380+
assert exc_info.value.original_error is not None
381+
382+
352383
def test_parse_extension_list_response_data_preserves_hyperbrowser_value_read_errors():
353384
class _BrokenValueLookupMapping(Mapping[str, object]):
354385
def __iter__(self) -> Iterator[str]:

tests/test_session_recording_utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,34 @@ def __getitem__(self, key: str) -> object:
282282
assert exc_info.value.original_error is not None
283283

284284

285+
def test_parse_session_recordings_response_data_falls_back_for_unreadable_recording_keys():
286+
class _BrokenKey(str):
287+
def __iter__(self):
288+
raise RuntimeError("cannot iterate recording key")
289+
290+
class _BrokenValueLookupMapping(Mapping[str, object]):
291+
def __iter__(self) -> Iterator[str]:
292+
yield _BrokenKey("type")
293+
294+
def __len__(self) -> int:
295+
return 1
296+
297+
def __getitem__(self, key: str) -> object:
298+
_ = key
299+
raise RuntimeError("cannot read recording value")
300+
301+
with pytest.raises(
302+
HyperbrowserError,
303+
match=(
304+
"Failed to read session recording object value "
305+
"for key '<unreadable key>' at index 0"
306+
),
307+
) as exc_info:
308+
parse_session_recordings_response_data([_BrokenValueLookupMapping()])
309+
310+
assert exc_info.value.original_error is not None
311+
312+
285313
def test_parse_session_recordings_response_data_preserves_hyperbrowser_value_read_errors():
286314
class _BrokenValueLookupMapping(Mapping[str, object]):
287315
def __iter__(self) -> Iterator[str]:

tests/test_tools_mapping_inputs.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,39 @@ def __getitem__(self, key: str) -> object:
222222
assert exc_info.value.original_error is None
223223

224224

225+
@pytest.mark.parametrize("runner", [_run_scrape_tool_sync, _run_scrape_tool_async])
226+
def test_tool_wrappers_fall_back_for_unreadable_param_value_read_keys(runner):
227+
class _BrokenKey(str):
228+
def __new__(cls, value: str):
229+
instance = super().__new__(cls, value)
230+
instance._iteration_count = 0
231+
return instance
232+
233+
def __iter__(self):
234+
self._iteration_count += 1
235+
if self._iteration_count > 1:
236+
raise RuntimeError("cannot iterate param key")
237+
return super().__iter__()
238+
239+
class _BrokenValueMapping(Mapping[str, object]):
240+
def __iter__(self) -> Iterator[str]:
241+
yield _BrokenKey("url")
242+
243+
def __len__(self) -> int:
244+
return 1
245+
246+
def __getitem__(self, key: str) -> object:
247+
_ = key
248+
raise RuntimeError("cannot read value")
249+
250+
with pytest.raises(
251+
HyperbrowserError, match="Failed to read tool param '<unreadable key>'"
252+
) as exc_info:
253+
runner(_BrokenValueMapping())
254+
255+
assert isinstance(exc_info.value.original_error, RuntimeError)
256+
257+
225258
@pytest.mark.parametrize("runner", [_run_scrape_tool_sync, _run_scrape_tool_async])
226259
def test_tool_wrappers_wrap_param_key_strip_failures(runner):
227260
class _BrokenStripKey(str):

0 commit comments

Comments
 (0)