Skip to content

Commit a0811f5

Browse files
Harden response utility operation-name and key validation
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 8c38292 commit a0811f5

2 files changed

Lines changed: 73 additions & 3 deletions

File tree

hyperbrowser/client/managers/response_utils.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@
44
from hyperbrowser.exceptions import HyperbrowserError
55

66
T = TypeVar("T")
7+
_MAX_OPERATION_NAME_DISPLAY_LENGTH = 120
8+
_TRUNCATED_OPERATION_NAME_SUFFIX = "... (truncated)"
9+
10+
11+
def _normalize_operation_name_for_error(operation_name: str) -> str:
12+
normalized_name = "".join(
13+
"?" if ord(character) < 32 or ord(character) == 127 else character
14+
for character in operation_name
15+
).strip()
16+
if not normalized_name:
17+
return "operation"
18+
if len(normalized_name) <= _MAX_OPERATION_NAME_DISPLAY_LENGTH:
19+
return normalized_name
20+
available_length = _MAX_OPERATION_NAME_DISPLAY_LENGTH - len(
21+
_TRUNCATED_OPERATION_NAME_SUFFIX
22+
)
23+
if available_length <= 0:
24+
return _TRUNCATED_OPERATION_NAME_SUFFIX
25+
return f"{normalized_name[:available_length]}{_TRUNCATED_OPERATION_NAME_SUFFIX}"
726

827

928
def parse_response_model(
@@ -14,23 +33,33 @@ def parse_response_model(
1433
) -> T:
1534
if not isinstance(operation_name, str) or not operation_name.strip():
1635
raise HyperbrowserError("operation_name must be a non-empty string")
36+
normalized_operation_name = _normalize_operation_name_for_error(operation_name)
1737
if not isinstance(response_data, Mapping):
18-
raise HyperbrowserError(f"Expected {operation_name} response to be an object")
38+
raise HyperbrowserError(
39+
f"Expected {normalized_operation_name} response to be an object"
40+
)
1941
try:
2042
response_payload = dict(response_data)
2143
except HyperbrowserError:
2244
raise
2345
except Exception as exc:
2446
raise HyperbrowserError(
25-
f"Failed to read {operation_name} response data",
47+
f"Failed to read {normalized_operation_name} response data",
2648
original_error=exc,
2749
) from exc
50+
for key in response_payload.keys():
51+
if isinstance(key, str):
52+
continue
53+
raise HyperbrowserError(
54+
"Expected "
55+
f"{normalized_operation_name} response object keys to be strings"
56+
)
2857
try:
2958
return model(**response_payload)
3059
except HyperbrowserError:
3160
raise
3261
except Exception as exc:
3362
raise HyperbrowserError(
34-
f"Failed to parse {operation_name} response",
63+
f"Failed to parse {normalized_operation_name} response",
3564
original_error=exc,
3665
) from exc

tests/test_response_utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,47 @@ def test_parse_response_model_rejects_non_mapping_payloads():
166166
)
167167

168168

169+
def test_parse_response_model_rejects_non_string_keys():
170+
with pytest.raises(
171+
HyperbrowserError,
172+
match="Expected basic operation response object keys to be strings",
173+
):
174+
parse_response_model(
175+
{1: True}, # type: ignore[dict-item]
176+
model=BasicResponse,
177+
operation_name="basic operation",
178+
)
179+
180+
181+
def test_parse_response_model_sanitizes_operation_name_in_errors():
182+
with pytest.raises(
183+
HyperbrowserError,
184+
match="Expected basic\\?operation response to be an object",
185+
):
186+
parse_response_model(
187+
["bad"], # type: ignore[arg-type]
188+
model=BasicResponse,
189+
operation_name="basic\toperation",
190+
)
191+
192+
193+
def test_parse_response_model_truncates_operation_name_in_errors():
194+
long_operation_name = "basic operation " + ("x" * 200)
195+
196+
with pytest.raises(
197+
HyperbrowserError,
198+
match=(
199+
r"Expected basic operation x+\.\.\. \(truncated\) "
200+
r"response to be an object"
201+
),
202+
):
203+
parse_response_model(
204+
["bad"], # type: ignore[arg-type]
205+
model=BasicResponse,
206+
operation_name=long_operation_name,
207+
)
208+
209+
169210
def test_parse_response_model_wraps_mapping_read_failures():
170211
with pytest.raises(
171212
HyperbrowserError,

0 commit comments

Comments
 (0)