aio-libs/aiohttp · error · AttributeError
EmptyStreamReader._on_chunk_received is read-only
Error message
EmptyStreamReader._on_chunk_received is read-only
What it means
EmptyStreamReader (the EMPTY_PAYLOAD singleton) shadows _on_chunk_received with a property whose setter always raises AttributeError. This is intentional: the empty payload never delivers chunks, and the singleton is shared across all responses, so attaching a per-response chunk hook would leak across requests. The guard makes any attempt to set the hook loud. aiohttp's own client_reqrep.py already short-circuits this case (`if self._traces and payload is not EMPTY_PAYLOAD`), so user code that hits it has bypassed that check.
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 c0ef574e29)
Solutions
- Guard the assignment: `if payload is not EMPTY_PAYLOAD: payload._on_chunk_received = fn`.
- Prefer the official trace_configs API on ClientSession instead of poking the payload hook.
- Reset state in test teardown to avoid singleton pollution.
- Treat AttributeError here as a programming error in your instrumentation, not an aiohttp bug.
Example fix
// before
payload._on_chunk_received = my_hook # fails for EMPTY_PAYLOAD
// after
from aiohttp.streams import EMPTY_PAYLOAD
if payload is not EMPTY_PAYLOAD:
payload._on_chunk_received = my_hook Defensive patterns
Strategy: validation
Validate before calling
from aiohttp.streams import EMPTY_PAYLOAD
def attach_chunk_hook(payload, hook):
if payload is EMPTY_PAYLOAD:
return # nothing to observe on an empty body
payload._on_chunk_received = hook Try / catch
try:
payload._on_chunk_received = hook
except AttributeError as e:
if 'read-only' in str(e):
return # EMPTY_PAYLOAD — skip
raise Prevention
- Always guard hook assignment with `payload is not EMPTY_PAYLOAD`.
- Prefer the official trace_configs API over poking the payload.
- Reset singleton-touching instrumentation in test teardown.
When it happens
Trigger: Directly assigning EMPTY_PAYLOAD._on_chunk_received = fn (e.g. a trace/metrics shim that does not check for EMPTY_PAYLOAD); reaching into client_reqrep internals; monkey-patching the singleton for testing.
Common situations: Custom trace context that sets the hook unconditionally on every response (including empty-body 204/304); test fixtures that attach hooks to the EMPTY_PAYLOAD singleton and leak across tests; instrumentation copied from an older aiohttp that lacked the guard.
Related errors
- Called begin_http_chunk_receiving when some data was already
- Called end_chunk_receiving without calling begin_chunk_recei
- Unsupported order {order!r}
- Connection closed.
- %s() called while another coroutine is already waiting for i
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/774c527150d169ba.json.
Report an issue: GitHub.