aio-libs/aiohttp · error · BadHttpMessage
Bad line ending, expected CRLF
Error message
Bad line ending, expected CRLF
What it means
Raised in strict (non-lax) mode when a bare LF is found in the buffered tail without a preceding CR. Strict parsing requires CRLF line terminators per RFC 9112; lax mode accepts bare LF. The parser rejects rather than buffering so that bytes from the following request do not leak into the error message.
Solutions
- Terminate every HTTP/1.1 line with CRLF (\r\n).
- Use a real HTTP client library instead of raw sockets.
- Do not switch the server parser to lax solely to tolerate bare LF.
Example fix
# before sock.send(b'GET / HTTP/1.1\nHost: a\n\n') # after sock.send(b'GET / HTTP/1.1\r\nHost: a\r\n\r\n')
Defensive patterns
Strategy: validation
Validate before calling
def uses_crlf(raw: bytes) -> bool:
# no bare LF not preceded by CR
return b'\r\n' in raw and b'\n' not in raw.replace(b'\r\n', b'') Type guard
def terminated_with_crlf(raw: bytes) -> bool:
stripped = raw.replace(b'\r\n', b'')
return b'\n' not in stripped and b'\r' not in stripped Try / catch
from aiohttp.http_exceptions import BadHttpMessage
try:
...parse...
except BadHttpMessage as e:
if 'expected CRLF' in str(e):
transport.close() Prevention
- Always emit \r\n in hand-written HTTP.
- When testing with netcat, configure it to send CRLF (e.g. 'set crlf' in telnet).
When it happens
Trigger: A client sends HTTP/1.1 request lines or headers terminated with \n instead of \r\n to the strict request parser (server-side default).
Common situations: Hand-written HTTP via socket.send(b'GET / HTTP/1.1\n'), netcat/telnet-style clients, certain buggy embedded clients, naive test scripts using '\n'.
Related errors
- Bad HTTP method in status line
- Bad status line
- Data after `Connection: close`
- Duplicate ' ' header found.
- Invalid HTTP header
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/2f7e854212ecab2a.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/http_parser.py:523
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 d041d4d0fd)