{"record":{"id":"ba92b76deebfa4d9","repo":"aio-libs/aiohttp","slug":"compressed-stream-has-more-than-max-decompress-me","errorCode":null,"errorMessage":"Compressed stream has more than {MAX_DECOMPRESS_MEMBERS} members","messagePattern":"Compressed stream has more than (.+?) members","errorType":"exception","errorClass":"TooManyMembersError","httpStatus":null,"severity":"error","filePath":"aiohttp/compression_utils.py","lineNumber":255,"sourceCode":"    @abstractmethod\n    def _new_decompressor(self) -> _DecompressObjT:\n        \"\"\"Return a decompressor for the next member.\"\"\"\n\n    def _decompress_members(self, first: bytes, max_length: int) -> bytes:\n        \"\"\"Decode the members following the one ``first`` came from.\"\"\"\n        remaining = memoryview(self._decompressor.unused_data)\n        parts = [first]\n        produced = len(first)\n        pos = 0\n        window = MEMBER_WINDOW_MIN\n        budget = max_length\n        members = 1\n\n        while pos < len(remaining):\n            if self._decompressor.eof:\n                members += 1\n                if members > MAX_DECOMPRESS_MEMBERS:\n                    raise TooManyMembersError(\n                        f\"Compressed stream has more than \"\n                        f\"{MAX_DECOMPRESS_MEMBERS} members\"\n                    )\n                # Replace the spent decompressor before the budget check below\n                # can break out of the loop: it still lists these bytes in its\n                # unused_data and would hand them back on the next call.\n                self._decompressor = self._new_decompressor()\n                window = MEMBER_WINDOW_MIN\n            if max_length != self._unlimited:\n                budget = max_length - produced\n                if budget <= 0:\n                    self._pending_unused_data = bytes(remaining[pos:])\n                    break\n\n            end = min(pos + window, len(remaining))\n            chunk = self._decompressor.decompress(remaining[pos:end], budget)\n            if chunk:\n                parts.append(chunk)","sourceCodeStart":237,"sourceCodeEnd":273,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/compression_utils.py#L237-L273","documentation":"Raised by the concat-stream decompressor (used for zstd and multi-member brotli) when a single compressed response contains more than MAX_DECOMPRESS_MEMBERS concatenated members. The cap exists because an attacker can pack a huge number of tiny members into one body to burn CPU/time on decompressor re-initialization. Breaching the limit aborts with TooManyMembersError before the resource abuse can continue.","triggerScenarios":"A server (or MITM) returns a Content-Encoding: zstd or br body that is many concatenated streams; a corrupted/truncated upload re-assembled into one body; a zip-bomb-style payload crafted against the decompressor.","commonSituations":"Misconfigured origin that fragments responses; malicious endpoint during scraping; proxy that re-encodes and concatenates; bug in upstream encoder producing thousands of frames.","solutions":["Catch TooManyMembersError and treat the response as malformed (do not retry the same body).","Restrict Accept-Encoding to identity or gzip when the upstream is untrusted.","Cap client_max_size / response body size so abusive payloads are rejected earlier.","Report the endpoint to the operator if it is a legitimate partner feeding malformed encodings."],"exampleFix":"# before\nresp = await session.get(url)\ndata = await resp.read()\n# after\nfrom aiohttp.compression_utils import TooManyMembersError\ntry:\n    data = await resp.read()\nexcept TooManyMembersError:\n    resp.close()\n    raise ClientPayloadError('too many compressed members')","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"null","tryCatchPattern":"from aiohttp.compression_utils import TooManyMembersError\ntry:\n    body = await resp.read()\nexcept TooManyMembersError:\n    resp.close()\n    log.warning('rejected multi-member stream from %s', resp.url)","preventionTips":["Treat TooManyMembersError as malformed-content, not a transient failure.","For untrusted upstreams, send Accept-Encoding: gzip, identity to avoid br/zstd concat streams.","Cap response body size at the session/server level to reject abusive payloads early."],"tags":["decompression","security","dos","zstd","brotli"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}