{"id":"774c527150d169ba","repo":"aio-libs/aiohttp","slug":"emptystreamreader-on-chunk-received-is-read-only","errorCode":null,"errorMessage":"EmptyStreamReader._on_chunk_received is read-only","messagePattern":"EmptyStreamReader\\._on_chunk_received is read-only","errorType":"validation","errorClass":"AttributeError","httpStatus":null,"severity":"error","filePath":"aiohttp/streams.py","lineNumber":617,"sourceCode":"\n    __slots__ = (\"_read_eof_chunk\",)\n\n    def __init__(self) -> None:\n        self._read_eof_chunk = False\n        self.total_bytes = 0\n\n    # Shadow the inherited slot with a property so the EMPTY_PAYLOAD singleton\n    # can't be polluted with a per-response hook that would leak across\n    # requests. EmptyStreamReader never delivers a chunk anyway.\n    @property\n    def _on_chunk_received(self) -> None:\n        return None\n\n    @_on_chunk_received.setter\n    def _on_chunk_received(\n        self, value: Callable[[bytes], Coroutine[None, None, None]] | None\n    ) -> None:\n        raise AttributeError(\"EmptyStreamReader._on_chunk_received is read-only\")\n\n    def __repr__(self) -> str:\n        return \"<%s>\" % self.__class__.__name__\n\n    def exception(self) -> BaseException | None:\n        return None\n\n    def set_exception(\n        self,\n        exc: type[BaseException] | BaseException,\n        exc_cause: BaseException = _EXC_SENTINEL,\n    ) -> None:\n        pass\n\n    def on_eof(self, callback: Callable[[], None]) -> None:\n        try:\n            callback()\n        except Exception:","sourceCodeStart":599,"sourceCodeEnd":635,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/streams.py#L599-L635","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\npayload._on_chunk_received = my_hook  # fails for EMPTY_PAYLOAD\n// after\nfrom aiohttp.streams import EMPTY_PAYLOAD\nif payload is not EMPTY_PAYLOAD:\n    payload._on_chunk_received = my_hook","handlingStrategy":"validation","validationCode":"from aiohttp.streams import EMPTY_PAYLOAD\n\ndef attach_chunk_hook(payload, hook):\n    if payload is EMPTY_PAYLOAD:\n        return  # nothing to observe on an empty body\n    payload._on_chunk_received = hook","typeGuard":null,"tryCatchPattern":"try:\n    payload._on_chunk_received = hook\nexcept AttributeError as e:\n    if 'read-only' in str(e):\n        return  # EMPTY_PAYLOAD — skip\n    raise","preventionTips":["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."],"tags":["streams","internal","singleton","instrumentation"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}