aio-libs/aiohttp · error · AttributeError

EmptyStreamReader._on_chunk_received is read-only

Error message

EmptyStreamReader._on_chunk_received is read-only

What it means

Raised by the _on_chunk_received setter on EmptyStreamReader (the EMPTY_PAYLOAD singleton) when something tries to assign a chunk-received hook to it. Because the empty reader is a shared singleton that never delivers chunks, attaching a per-response hook would leak across requests, so the attribute is shadowed as a read-only property. This is a defensive guard, not normal control flow.

Solutions

  1. Guard the assignment: if not isinstance(response.content, EmptyStreamReader): response.content._on_chunk_received = cb.
  2. Set the hook via the public trace_config / response API rather than poking the private attribute.
  3. Skip instrumentation for responses with no body (status 204, 304, or Content-Length 0).

Example fix

# before
response.content._on_chunk_received = on_chunk

# after
from aiohttp.streams import EmptyStreamReader
if not isinstance(response.content, EmptyStreamReader):
    response.content._on_chunk_received = on_chunk
Defensive patterns

Strategy: type-guard

Validate before calling

from aiohttp.streams import EmptyStreamReader
if not isinstance(response.content, EmptyStreamReader):
    response.content._on_chunk_received = on_chunk

Type guard

from aiohttp.streams import EmptyStreamReader
def is_empty_stream(stream) -> bool:
    return isinstance(stream, EmptyStreamReader)

Try / catch

try:
    response.content._on_chunk_received = on_chunk
except AttributeError:
    pass  # empty singleton; nothing to observe

Prevention

When it happens

Trigger: Calling response.content._on_chunk_received = cb (or aiohttp assigning it internally) when response.content is the EMPTY_PAYLOAD singleton; instrumenting a stream that turned out to be empty (e.g. a 204 No Content or empty body).

Common situations: Observability/tracing middleware that unconditionally sets a chunk hook on every response body; libraries that wrap content for metrics; tests that set hooks on synthetic empty responses.

Related errors


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

Appendix: source

Thrown at aiohttp/streams.py:617

    __slots__ = ("_read_eof_chunk",)

    def __init__(self) -> None:
        self._read_eof_chunk = False
        self.total_bytes = 0

    # Shadow the inherited slot with a property so the EMPTY_PAYLOAD singleton
    # can't be polluted with a per-response hook that would leak across
    # requests. EmptyStreamReader never delivers a chunk anyway.
    @property
    def _on_chunk_received(self) -> None:
        return None

    @_on_chunk_received.setter
    def _on_chunk_received(
        self, value: Callable[[bytes], Coroutine[None, None, None]] | None
    ) -> None:
        raise AttributeError("EmptyStreamReader._on_chunk_received is read-only")

    def __repr__(self) -> str:
        return "<%s>" % self.__class__.__name__

    def exception(self) -> BaseException | None:
        return None

    def set_exception(
        self,
        exc: type[BaseException] | BaseException,
        exc_cause: BaseException = _EXC_SENTINEL,
    ) -> None:
        pass

    def on_eof(self, callback: Callable[[], None]) -> None:
        try:
            callback()
        except Exception:

View on GitHub (pinned to d041d4d0fd)