aio-libs/aiohttp · error · BadHttpMessage

Data after `Connection: close`

Error message

Data after `Connection: close`

What it means

Raised mid-parse when should_close is set (the peer signalled Connection: close, or another close condition) and additional non-empty lines arrive on the same connection. After a close-framed message the connection must carry nothing more; trailing bytes indicate pipeline injection or a peer that did not honor the close.

Solutions

  1. Close the socket immediately after sending/receiving Connection: close.
  2. Do not pipeline further requests after signalling close.
  3. Upgrade the client to honor connection lifecycle semantics.

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

# cannot validate remote bytes; instead, ensure your own client closes after Connection: close
import socket
sock.sendall(req_with_connection_close)
# read the single response, then:
sock.shutdown(socket.SHUT_WR)
sock.close()

Type guard

null

Try / catch

from aiohttp.http_exceptions import BadHttpMessage
try:
    await parser.feed_data(chunk)
except BadHttpMessage as e:
    # 'Data after Connection: close' -> terminate the transport
    transport.close()

Prevention

When it happens

Trigger: A peer sends Connection: close (or triggers should_close) and then keeps writing request lines or data on the same socket.

Common situations: HTTP keep-alive confusion, clients pipelining after a close signal, half-closed socket misuse, broken clients that ignore Connection: close.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/691846c3489052e5. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/http_parser.py:372

                    self._max_msg_queue_size
                    and self._msg_in_flight >= self._max_msg_queue_size
                ):
                    # Queue full: buffer the rest and stop. Safe pause point;
                    # any preceding body is consumed before the next request
                    # line. Resumes via feed_data(b"") when the queue drains.
                    self._tail = data[start_pos:]
                    # The remainder now lives in self._tail only. Don't return it.
                    data = EMPTY
                    break
                pos = data.find(SEP, start_pos)
                # consume \r\n
                if pos == start_pos and not self._lines:
                    start_pos = pos + len(SEP)
                    continue

                if pos >= start_pos:
                    if should_close:
                        raise BadHttpMessage("Data after `Connection: close`")

                    # line found
                    line = data[start_pos:pos]
                    if SEP == b"\n":  # For lax response parsing
                        line = line.rstrip(b"\r")
                    if len(line) > max_line_length:
                        raise LineTooLong(line[:100] + b"...", max_line_length)

                    self._lines.append(line)
                    # After processing the status/request line, everything is a header.
                    max_line_length = self.max_field_size

                    if len(self._lines) > self.max_headers:
                        raise BadHttpMessage("Too many headers received")

                    start_pos = pos + len(SEP)

                    # \r\n\r\n found

View on GitHub (pinned to d041d4d0fd)