aio-libs/aiohttp · error · BadHttpMessage
Bad line ending, expected CRLF
Error message
Bad line ending, expected CRLF
What it means
Raised by HttpParser.feed_data (aiohttp/http_parser.py:521) while buffering an incomplete line: if the pending tail contains a bare '\n' the parser concludes the peer is using LF instead of the CRLF that RFC 9112 section 2.2 mandates, and rejects it. Rejecting (rather than tolerating LF) prevents a following request's bytes from being appended to the current line and leaking into error text. The lax response parser uses '\n' as its separator, so responses tolerate bare LF; this fires in the strict request parser.
Source
Thrown at aiohttp/http_parser.py:521
elif upgraded:
# No body to read, so the connection switches to
# the upgraded protocol immediately.
self._upgraded = True
payload = EMPTY_PAYLOAD
else:
payload = EMPTY_PAYLOAD
messages.append((msg, payload))
if self._max_msg_queue_size:
self._msg_in_flight += 1
should_close = msg.should_close
else:
self._tail = data[start_pos:]
# A bare LF here means CRLF was required:
# reject instead of buffering, else a following request's
# bytes get appended to this line and leak in the error.
if b"\n" in self._tail:
raise BadHttpMessage("Bad line ending, expected CRLF")
if len(self._tail) > self.max_line_size:
raise LineTooLong(self._tail[:100] + b"...", self.max_line_size)
data = EMPTY
break
# no parser, just store
elif self._payload_parser is None and self._upgraded:
assert not self._lines
break
# feed payload
else:
assert not self._lines
assert self._payload_parser is not None
try:
payload_state, data = self._payload_parser.feed_data(
data[start_pos:], SEP
)View on GitHub (pinned to c0ef574e29)
Solutions
- Terminate every HTTP line with CRLF ('\r\n'), including the blank line that ends the header block.
- If writing a raw client, send '\r\n\r\n' explicitly - do not rely on platform line endings or print()'s newline.
- Use a real HTTP library (like aiohttp) instead of hand-crafted sockets.
Example fix
# before - bare LF line endings sock.send(b'GET / HTTP/1.1\nHost: x\n\n') # after - proper CRLF sock.send(b'GET / HTTP/1.1\r\nHost: x\r\n\r\n')
Defensive patterns
Strategy: validation
Validate before calling
def crlf_terminated(raw: bytes) -> bool:
# every line must end with \r\n; no bare \n
return b'\n' in raw and all(
seg.endswith(b'\r') or i == len(parts) - 1 and seg == b''
for i, seg in enumerate((raw.split(b'\n')))
)
# simpler: assert no '\n' that is not preceded by '\r'
def no_bare_lf(raw: bytes) -> bool:
return b'\r\n'.join(raw.split(b'\n')) == raw
assert no_bare_lf(outgoing) Prevention
- Always terminate HTTP lines with CRLF (\r\n)
- Use an HTTP library rather than raw sockets
- Never rely on platform line endings for HTTP
When it happens
Trigger: A client/server that terminates HTTP lines with '\n' instead of '\r\n' in a request line or header line. E.g. a raw socket sending 'GET / HTTP/1.0\n\n'. Strict request parsing refuses this; lax response parsing accepts it.
Common situations: Raw netcat/nc scripts, telnet sessions, embedded clients that build HTTP by hand with platform line endings (\n on Unix), text-mode transfers that corrupt CRLF into LF, or HTTP/1.0-only embedded stacks.
Related errors
- Duplicate '{name}' header found.
- Bad HTTP method in status line {line!r}
- Bad status line {line!r}
- Missing 'Host' header in request.
- Request has duplicate `chunked` Transfer-Encoding
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/2f7e854212ecab2a.json.
Report an issue: GitHub.