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
- Guard the assignment: if not isinstance(response.content, EmptyStreamReader): response.content._on_chunk_received = cb.
- Set the hook via the public trace_config / response API rather than poking the private attribute.
- 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
- Guard chunk-hook assignment with an EmptyStreamReader isinstance check.
- Use trace_config for instrumentation rather than private attributes.
- Skip body instrumentation for 204/304/empty responses.
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
- Called while some coroutine is waiting for incoming data.
- () called while another coroutine is already waiting for…
- Cannot call .write() for websocket
- Connection closed.
- Got more than bytes when reading: .
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)