aio-libs/aiohttp · error · RuntimeError

Called end_chunk_receiving without calling begin_chunk_recei

Error message

Called end_chunk_receiving without calling begin_chunk_receiving first

What it means

Internal guard in StreamReader.end_http_chunk_receiving: it requires _http_chunk_splits to be non-None, i.e. begin_http_chunk_receiving was called first. Calling end without begin indicates a parser-state mismatch — the parser emitted a chunk-end marker with no matching begin. Like 185, this is an aiohttp-internal invariant rather than something typical application code triggers.

Source

Thrown at aiohttp/streams.py:315

        if waiter is not None:
            self._waiter = None
            set_result(waiter, None)

        if self._size > self._high_water:
            self._protocol.pause_reading()
        return False

    def begin_http_chunk_receiving(self) -> None:
        if self._http_chunk_splits is None:
            if self.total_bytes:
                raise RuntimeError(
                    "Called begin_http_chunk_receiving when some data was already fed"
                )
            self._http_chunk_splits = collections.deque()

    def end_http_chunk_receiving(self) -> None:
        if self._http_chunk_splits is None:
            raise RuntimeError(
                "Called end_chunk_receiving without calling "
                "begin_chunk_receiving first"
            )

        # self._http_chunk_splits contains logical byte offsets from start of
        # the body transfer. Each offset is the offset of the end of a chunk.
        # "Logical" means bytes, accessible for a user.
        # If no chunks containing logical data were received, current position
        # is difinitely zero.
        pos = self._http_chunk_splits[-1] if self._http_chunk_splits else 0

        if self.total_bytes == pos:
            # We should not add empty chunks here. So we check for that.
            # Note, when chunked + gzip is used, we can receive a chunk
            # of compressed data, but that data may not be enough for gzip FSM
            # to yield any uncompressed data. That's why current position may
            # not change after receiving a chunk.
            return

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Inspect raw upstream bytes to confirm chunk framing is well-formed (size CRLF data CRLF ...).
  2. Avoid calling the chunk API manually — delegate to the HTTP parser.
  3. Upgrade aiohttp/llhttp; if it reproduces, file an issue with full response capture.
  4. For custom StreamReader feeding, always bracket with begin/end and never call end twice.
Defensive patterns

Strategy: try-catch

Try / catch

try:
    body = await resp.read()
except RuntimeError as e:
    if 'end_chunk_receiving' in str(e):
        log.warning('upstream chunk framing mismatch on %s', url)
        raise BadGateway('malformed chunked response') from e
    raise

Prevention

When it happens

Trigger: The HTTP parser (http_parser.py:1045) calls payload.end_http_chunk_receiving() while _http_chunk_splits is None — e.g. malformed chunked body missing the chunk-size line, or a custom protocol object that calls end_http_chunk_receiving without first calling begin_http_chunk_receiving.

Common situations: Malformed upstream chunked body (server bug); a man-in-the-middle truncating chunk framing; a custom request/response feeding path that mishandles the chunk API; race between parser version and streams internals.

Related errors


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