Skip to content

Commit fc0b848

Browse files
yujclaude
andcommitted
Clarify LineTooLong error messages with context label
The `LineTooLong` exception now accepts an optional `context` parameter describing which part of the HTTP message exceeded the size limit (e.g. `"request URL"`, `"status reason phrase"`, `"header field name"`, `"header field value"`, `"request/status line"`, `"trailer line"`, `"chunk size line"`, `"header line with continuations"`, or `"stream until separator"`). Every `LineTooLong` call site in the pure-Python parser, the Cython parser, and `StreamReader.readuntil` now passes a descriptive context. Fixes the confusing "Status line is too long" error reported in #7177 when the actual issue is a too-long URL. Co-authored-by: Claude <[email protected]>
1 parent 53f6e91 commit fc0b848

7 files changed

Lines changed: 102 additions & 18 deletions

File tree

CHANGES/7177.bugfix.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Improved the :exc:`~aiohttp.http_exceptions.LineTooLong` error message to
2+
describe which part of the HTTP message exceeded the limit (e.g. the request
3+
URL, status reason phrase, header field name/value, request/status line, or
4+
trailer). Previously, every overflow reported a generic ``"when reading:"``
5+
prefix, which was confusing when only the URL was too long but the message was
6+
historically labeled as a status-line overflow -- by :user:`Ricardo-M-L`.

aiohttp/_http_parser.pyx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -773,8 +773,10 @@ cdef int cb_on_url(cparser.llhttp_t* parser,
773773
cdef HttpParser pyparser = <HttpParser>parser.data
774774
try:
775775
if length > pyparser._max_line_size:
776-
status = pyparser._buf + at[:length]
777-
raise LineTooLong(status[:100] + b"...", pyparser._max_line_size)
776+
url = pyparser._buf + at[:length]
777+
raise LineTooLong(
778+
url[:100] + b"...", pyparser._max_line_size, "request URL"
779+
)
778780
extend(pyparser._buf, at, length)
779781
except BaseException as ex:
780782
pyparser._last_error = ex
@@ -789,7 +791,11 @@ cdef int cb_on_status(cparser.llhttp_t* parser,
789791
try:
790792
if length > pyparser._max_line_size:
791793
reason = pyparser._buf + at[:length]
792-
raise LineTooLong(reason[:100] + b"...", pyparser._max_line_size)
794+
raise LineTooLong(
795+
reason[:100] + b"...",
796+
pyparser._max_line_size,
797+
"status reason phrase",
798+
)
793799
extend(pyparser._buf, at, length)
794800
except BaseException as ex:
795801
pyparser._last_error = ex
@@ -807,7 +813,11 @@ cdef int cb_on_header_field(cparser.llhttp_t* parser,
807813
size = len(pyparser._raw_name) + length
808814
if size > pyparser._max_field_size:
809815
name = pyparser._raw_name + at[:length]
810-
raise LineTooLong(name[:100] + b"...", pyparser._max_field_size)
816+
raise LineTooLong(
817+
name[:100] + b"...",
818+
pyparser._max_field_size,
819+
"header field name",
820+
)
811821
pyparser._header_name_size = size
812822
pyparser._on_header_field(at, length)
813823
except BaseException as ex:
@@ -825,7 +835,11 @@ cdef int cb_on_header_value(cparser.llhttp_t* parser,
825835
size = len(pyparser._raw_value) + length
826836
if pyparser._header_name_size + size > pyparser._max_field_size:
827837
value = pyparser._raw_value + at[:length]
828-
raise LineTooLong(value[:100] + b"...", pyparser._max_field_size)
838+
raise LineTooLong(
839+
value[:100] + b"...",
840+
pyparser._max_field_size,
841+
"header field value",
842+
)
829843
pyparser._on_header_value(at, length)
830844
except BaseException as ex:
831845
pyparser._last_error = ex

aiohttp/http_exceptions.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,13 @@ class ContentLengthError(PayloadEncodingError):
7474

7575

7676
class LineTooLong(BadHttpMessage):
77-
def __init__(self, line: bytes, limit: int) -> None:
78-
super().__init__(f"Got more than {limit} bytes when reading: {line!r}.")
79-
self.args = (line, limit)
77+
def __init__(
78+
self, line: bytes, limit: int, context: str = "line"
79+
) -> None:
80+
super().__init__(
81+
f"Got more than {limit} bytes when reading {context}: {line!r}."
82+
)
83+
self.args = (line, limit, context)
8084

8185

8286
class InvalidHeader(BadHttpMessage):

aiohttp/http_parser.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,9 @@ def parse_headers(
207207
if header_length > self.max_field_size:
208208
header_line = bname + b": " + b"".join(bvalue_lst)
209209
raise LineTooLong(
210-
header_line[:100] + b"...", self.max_field_size
210+
header_line[:100] + b"...",
211+
self.max_field_size,
212+
"header line with continuations",
211213
)
212214
bvalue_lst.append(line)
213215

@@ -354,7 +356,13 @@ def feed_data(
354356
if SEP == b"\n": # For lax response parsing
355357
line = line.rstrip(b"\r")
356358
if len(line) > max_line_length:
357-
raise LineTooLong(line[:100] + b"...", max_line_length)
359+
raise LineTooLong(
360+
line[:100] + b"...",
361+
max_line_length,
362+
"request/status line"
363+
if not self._lines
364+
else "header line",
365+
)
358366

359367
self._lines.append(line)
360368
# After processing the status/request line, everything is a header.
@@ -487,7 +495,11 @@ def get_content_length() -> int | None:
487495
else:
488496
self._tail = data[start_pos:]
489497
if len(self._tail) > self.max_line_size:
490-
raise LineTooLong(self._tail[:100] + b"...", self.max_line_size)
498+
raise LineTooLong(
499+
self._tail[:100] + b"...",
500+
self.max_line_size,
501+
"request/status line",
502+
)
491503
data = EMPTY
492504
break
493505

@@ -922,7 +934,11 @@ def feed_data(
922934
max_line_length = self._max_field_size
923935
if len(self._chunk_tail) > max_line_length:
924936
raise LineTooLong(
925-
self._chunk_tail[:100] + b"...", max_line_length
937+
self._chunk_tail[:100] + b"...",
938+
max_line_length,
939+
"chunk size line"
940+
if self._chunk != ChunkState.PARSE_TRAILERS
941+
else "trailer line",
926942
)
927943

928944
chunk = self._chunk_tail + chunk
@@ -1020,7 +1036,11 @@ def feed_data(
10201036
line = line.rstrip(b"\r")
10211037

10221038
if len(line) > self._max_field_size:
1023-
raise LineTooLong(line[:100] + b"...", self._max_field_size)
1039+
raise LineTooLong(
1040+
line[:100] + b"...",
1041+
self._max_field_size,
1042+
"trailer line",
1043+
)
10241044

10251045
self._trailer_lines.append(line)
10261046

aiohttp/streams.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,9 @@ async def readuntil(
395395
not_enough = False
396396

397397
if chunk_size > max_size:
398-
raise LineTooLong(chunk[:100] + b"...", max_size)
398+
raise LineTooLong(
399+
chunk[:100] + b"...", max_size, "stream until separator"
400+
)
399401

400402
if self._eof:
401403
break

tests/test_http_exceptions.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,16 @@ class TestLineTooLong:
7979
def test_ctor(self) -> None:
8080
err = http_exceptions.LineTooLong(b"spam", 10)
8181
assert err.code == 400
82-
assert err.message == "Got more than 10 bytes when reading: b'spam'."
82+
assert err.message == "Got more than 10 bytes when reading line: b'spam'."
83+
assert err.headers is None
84+
85+
def test_ctor_with_context(self) -> None:
86+
err = http_exceptions.LineTooLong(b"spam", 10, "request URL")
87+
assert err.code == 400
88+
assert (
89+
err.message
90+
== "Got more than 10 bytes when reading request URL: b'spam'."
91+
)
8392
assert err.headers is None
8493

8594
def test_pickle(self) -> None:
@@ -89,20 +98,35 @@ def test_pickle(self) -> None:
8998
pickled = pickle.dumps(err, proto)
9099
err2 = pickle.loads(pickled)
91100
assert err2.code == 400
92-
assert err2.message == ("Got more than 10 bytes when reading: b'spam'.")
101+
assert err2.message == (
102+
"Got more than 10 bytes when reading line: b'spam'."
103+
)
93104
assert err2.headers is None
94105
assert err2.foo == "bar"
95106

107+
def test_pickle_with_context(self) -> None:
108+
err = http_exceptions.LineTooLong(
109+
line=b"spam", limit=10, context="request URL"
110+
)
111+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
112+
pickled = pickle.dumps(err, proto)
113+
err2 = pickle.loads(pickled)
114+
assert err2.message == (
115+
"Got more than 10 bytes when reading request URL: b'spam'."
116+
)
117+
96118
def test_str(self) -> None:
97119
err = http_exceptions.LineTooLong(line=b"spam", limit=10)
98-
expected = "400, message:\n Got more than 10 bytes when reading: b'spam'."
120+
expected = (
121+
"400, message:\n Got more than 10 bytes when reading line: b'spam'."
122+
)
99123
assert str(err) == expected
100124

101125
def test_repr(self) -> None:
102126
err = http_exceptions.LineTooLong(line=b"spam", limit=10)
103127
assert repr(err) == (
104128
'<LineTooLong: 400, message="Got more than '
105-
"10 bytes when reading: b'spam'.\">"
129+
"10 bytes when reading line: b'spam'.\">"
106130
)
107131

108132

tests/test_http_parser.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,20 @@ def test_http_request_max_status_line(parser: HttpRequestParser, size: int) -> N
15521552
parser.feed_data(b"GET /path" + path + b" HTTP/1.1\r\n\r\n")
15531553

15541554

1555+
def test_http_request_max_status_line_mentions_request_line(
1556+
parser: HttpRequestParser,
1557+
) -> None:
1558+
path = b"t" * 8186
1559+
with pytest.raises(http_exceptions.LineTooLong) as exc_info:
1560+
parser.feed_data(b"GET /path" + path + b" HTTP/1.1\r\n\r\n")
1561+
# Error message must describe what overflowed (not "status line" when
1562+
# the request line is too long). See gh-7177.
1563+
assert (
1564+
"request URL" in exc_info.value.message
1565+
or "request/status line" in exc_info.value.message
1566+
)
1567+
1568+
15551569
def test_http_request_max_status_line_under_limit(parser: HttpRequestParser) -> None:
15561570
path = b"t" * 8172
15571571
messages, upgraded, tail = parser.feed_data(

0 commit comments

Comments
 (0)