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

  1. Terminate every HTTP/1.1 line with CRLF (\r\n).
  2. Use a real HTTP client library instead of raw sockets.
  3. 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

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


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)