{"id":"7090a07c2677d418","repo":"encode/httpx","slug":"attempted-to-read-or-stream-content-but-the-strea","errorCode":null,"errorMessage":"Attempted to read or stream content, but the stream has been closed.","messagePattern":"Attempted to read or stream content, but the stream has been closed\\.","errorType":"exception","errorClass":"StreamClosed","httpStatus":null,"severity":"error","filePath":"httpx/_content.py","lineNumber":100,"sourceCode":"            chunk = await self._stream.aread(self.CHUNK_SIZE)\n            while chunk:\n                yield chunk\n                chunk = await self._stream.aread(self.CHUNK_SIZE)\n        else:\n            # Otherwise iterate.\n            async for part in self._stream:\n                yield part\n\n\nclass UnattachedStream(AsyncByteStream, SyncByteStream):\n    \"\"\"\n    If a request or response is serialized using pickle, then it is no longer\n    attached to a stream for I/O purposes. Any stream operations should result\n    in `httpx.StreamClosed`.\n    \"\"\"\n\n    def __iter__(self) -> Iterator[bytes]:\n        raise StreamClosed()\n\n    async def __aiter__(self) -> AsyncIterator[bytes]:\n        raise StreamClosed()\n        yield b\"\"  # pragma: no cover\n\n\ndef encode_content(\n    content: str | bytes | Iterable[bytes] | AsyncIterable[bytes],\n) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:\n    if isinstance(content, (bytes, str)):\n        body = content.encode(\"utf-8\") if isinstance(content, str) else content\n        content_length = len(body)\n        headers = {\"Content-Length\": str(content_length)} if body else {}\n        return headers, ByteStream(body)\n\n    elif isinstance(content, Iterable) and not isinstance(content, dict):\n        # `not isinstance(content, dict)` is a bit oddly specific, but it\n        # catches a case that's easy for users to make in error, and would","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/encode/httpx/blob/b5addb64f0161ff6bfe94c124ef76f6a1fba5254/httpx/_content.py#L82-L118","documentation":"This is httpx.StreamClosed raised from UnattachedStream.__iter__ (sync). An UnattachedStream is the sentinel object httpx installs on a Request/Response after it has been pickled (see Request.__setstate__ / Response.__setstate__). The original I/O stream cannot survive serialization, so any synchronous iteration attempt on a deserialized object is treated as an invalid stream operation.","triggerScenarios":"Pickle/multiprocessing a Request or Response and then calling .read(), .iter_bytes(), or iterating .stream on the restored object synchronously; using a joblib/ProcessPoolExecutor worker that receives a serialized httpx Response and tries to stream its body; copy.deepcopy on a Response in some configurations.","commonSituations":"Passing httpx responses across process boundaries (Celery, multiprocessing, dask); caching layers that pickle responses; test fixtures that pickle/unpickle responses for replay; distributed task queues shipping HTTP responses.","solutions":["Read the body fully (`response.read()` / `response.content`) BEFORE serializing the Response, then send the bytes separately.","Do not pass Request/Response objects across process boundaries; instead pass primitive data (status, headers dict, content bytes) and reconstruct.","If you must pickle, extract `resp.content`, `resp.status_code`, `dict(resp.headers)` and rebuild a Response with content=... on the other side.","Use a contextvar/thread-local to keep the live Response in one process and only pass derived data to workers."],"exampleFix":"// before\nimport pickle, httpx\nresp = httpx.get('https://example.com')\nblob = pickle.dumps(resp)\nr = pickle.loads(blob)\nfor chunk in r.iter_bytes():  # StreamClosed\n    ...\n// after\nresp = httpx.get('https://example.com')\nbody = resp.content  # materialize first\npayload = {'status': resp.status_code, 'headers': dict(resp.headers), 'body': body}\nblob = pickle.dumps(payload)\n# in the worker:\np = pickle.loads(blob)\nr = httpx.Response(p['status'], headers=p['headers'], content=p['body'])","handlingStrategy":"validation","validationCode":"# Detect an unattached stream before iterating\nfrom httpx._content import UnattachedStream\nif isinstance(getattr(obj, 'stream', None), UnattachedStream):\n    raise RuntimeError('object was pickled; stream is detached')","typeGuard":"from httpx._content import UnattachedStream\n\ndef is_stream_live(httpx_obj) -> bool:\n    return not isinstance(getattr(httpx_obj, 'stream', None), UnattachedStream)","tryCatchPattern":"try:\n    for chunk in resp.iter_bytes():\n        ...\nexcept httpx.StreamClosed:\n    # object was deserialized; fall back to .content if materialized\n    body = getattr(resp, '_content', b'')","preventionTips":["Never pickle a live httpx Request/Response; serialize derived data instead.","Call response.read() before crossing a process boundary.","Pass (status_code, headers, body bytes) tuples to workers.","Use shared memory or queues for large bodies rather than pickle."],"tags":["streaming","pickle","serialization","multiprocessing"],"analyzedSha":"b5addb64f0161ff6bfe94c124ef76f6a1fba5254","analyzedAt":"2026-08-04T19:32:56.768Z","schemaVersion":2}