Decode every member of multi-member gzip bodies by APX403 · Pull Request #8485 · mitmproxy/mitmproxy · GitHub
Skip to content

Decode every member of multi-member gzip bodies - #8485

Open
APX403 wants to merge 4 commits into
mitmproxy:mainfrom
APX403:gzip-members
Open

APX403 wants to merge 4 commits into
mitmproxy:mainfrom
APX403:gzip-members

Conversation

@APX403

@APX403 APX403 commented Oct 16, 2026

Copy link
Copy Markdown

Description

Since #7940 (12.2.1), decode_gzip uses a single zlib.decompressobj, which stops at the end of the first gzip member. RFC 1952 allows a gzip stream to consist of several members, and gzip.GzipFile, used before, read all of them. Now the rest of the body is silently dropped: .content, .text, content views and the ~b filters only see the first member, and an addon that edits .text re-encodes and forwards the truncated body.

The decoder now keeps going while the remaining data starts with a gzip header. It feeds zlib bounded chunks, because unused_data copies everything after the end of a member, so passing the whole rest of the body each time would make bodies with many small members quadratic to decode. zlib-wrapped bodies, which #7940 added support for, are decoded as before.

Checklist

  • I have updated tests where applicable.
  • I have added an entry to the CHANGELOG.

APX403 and others added 2 commits October 14, 2026 02:22
Since decode_gzip switched to a single zlib.decompressobj (mitmproxy#7940), it
stops at the end of the first gzip member and drops the rest, while
gzip.GzipFile, used before, read every member (RFC 1952 allows any
number of them). Keep decoding while the remaining data starts with a
gzip header.

The input is fed to zlib in chunks of at most 16 KiB: unused_data
copies everything that follows the end of a member, so passing the
whole remaining body would make decoding quadratic in the number of
members.

@kayaal34 kayaal34 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran the tests locally (test_encoding.py, 16 passed) and compared a few edge cases with Python's gzip module:

  • two members: b"hello world", same as gzip.decompress
  • trailing zero bytes: first member only, same as gzip.decompress
  • trailing garbage after a member: returns the first member, while gzip.decompress raises BadGzipFile
  • second member truncated: returns the first member, while gzip.decompress raises EOFError

Decoding 20,000 one-byte members took about 0.05s, so the chunked feeding avoids the quadratic case.

Question: is silently dropping a truncated second member intended? It matches how a single truncated member is handled (test_truncated), so it may well be the right call for a proxy, but it could be worth a short comment in the code.

(I used an AI assistant to help go through the change; I ran the tests myself.)

It is not obvious from the loop what happens when the body ends inside a later member.
@APX403

APX403 commented Oct 23, 2026

Copy link
Copy Markdown
Author

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants