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

  1. Terminate every HTTP line with CRLF ('\r\n'), including the blank line that ends the header block.
  2. If writing a raw client, send '\r\n\r\n' explicitly - do not rely on platform line endings or print()'s newline.
  3. 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

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


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/2f7e854212ecab2a.json. Report an issue: GitHub.