Skip to content

Commit 5b5a3d8

Browse files
committed
šŸ› Fix HTTP error reason handling by truncating messages and replacing newlines; add tests for safe_status_message
1 parent da1e85f commit 5b5a3d8

File tree

2 files changed

+68
-2
lines changed

2 files changed

+68
-2
lines changed

ā€Žpackages/service-library/src/servicelib/aiohttp/rest_responses.pyā€Ž

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ def create_http_error(
9292
)
9393

9494
return http_error_cls(
95-
# Multiline not allowed in HTTP reason
96-
reason=reason.replace("\n", " ") if reason else None,
95+
reason=safe_status_message(reason),
9796
text=json_dumps(
9897
payload,
9998
),
@@ -129,3 +128,18 @@ def _pred(obj) -> bool:
129128
assert len(http_statuses) == len(found), "No duplicates" # nosec
130129

131130
return http_statuses
131+
132+
133+
def safe_status_message(message: str | None, max_length: int = 1500) -> str | None:
134+
"""
135+
Truncates a status-message (i.e. `reason` in HTTP errors) to a maximum length, replacing newlines with spaces.
136+
137+
This prevents issues such as:
138+
- `aiohttp.http_exceptions.LineTooLong`: 400, message: Got more than 8190 bytes when reading Status line is too long.
139+
- Multiline not allowed in HTTP reason attribute (aiohttp now raises ValueError).
140+
141+
See:
142+
- [RFC 9112, Section 4.1: HTTP/1.1 Message Syntax and Routing](https://datatracker.ietf.org/doc/html/rfc9112#section-4.1) (status line length limits)
143+
- [RFC 9110, Section 15.5: Reason Phrase](https://datatracker.ietf.org/doc/html/rfc9110#section-15.5) (reason phrase definition)
144+
"""
145+
return message.replace("\n", " ")[:max_length] if message else None

ā€Žpackages/service-library/tests/aiohttp/test_rest_responses.pyā€Ž

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,55 @@ def tests_exception_to_response(skip_details: bool, error_code: ErrorCodeStr | N
9797
assert response_json["error"]["message"] == expected_reason
9898
assert response_json["error"]["supportId"] == error_code
9999
assert response_json["error"]["status"] == response.status
100+
101+
102+
@pytest.mark.parametrize(
103+
"input_message, expected_output",
104+
[
105+
(None, None), # None input returns None
106+
("", None), # Empty string returns None
107+
("Simple message", "Simple message"), # Simple message stays the same
108+
(
109+
"Message\nwith\nnewlines",
110+
"Message with newlines",
111+
), # Newlines are replaced with spaces
112+
("A" * 2000, "A" * 1500), # Long message gets truncated to max_length (1500)
113+
(
114+
"Line1\nLine2\nLine3" + "X" * 1500,
115+
"Line1 Line2 Line3" + "X" * 1483,
116+
), # Combined case: newlines and truncation
117+
],
118+
ids=[
119+
"none_input",
120+
"empty_string",
121+
"simple_message",
122+
"newlines_replaced",
123+
"long_message_truncated",
124+
"newlines_and_truncation",
125+
],
126+
)
127+
def test_safe_status_message(input_message: str | None, expected_output: str | None):
128+
from servicelib.aiohttp.rest_responses import safe_status_message
129+
130+
result = safe_status_message(input_message)
131+
assert result == expected_output
132+
133+
# Test with custom max_length
134+
custom_max = 10
135+
result_custom = safe_status_message(input_message, max_length=custom_max)
136+
137+
# Check length constraint is respected
138+
if result_custom is not None:
139+
assert len(result_custom) <= custom_max
140+
141+
# Verify it can be used in a web response without raising exceptions
142+
try:
143+
# This would fail with long or multiline reasons
144+
if result is not None:
145+
web.Response(reason=result)
146+
147+
# Test with custom length result too
148+
if result_custom is not None:
149+
web.Response(reason=result_custom)
150+
except ValueError:
151+
pytest.fail("safe_status_message result caused an exception in web.Response")

0 commit comments

Comments
Ā (0)