Content-encoding and content-type name different things
Content-type describes the media type of the body, and content-encoding names the coding applied to it for transport. A response labeled as JSON with content-encoding set to gzip carries JSON that has been compressed; the receiver decompresses and then parses the JSON. The two headers are independent, and a body can carry one, both or neither.
An absent content-encoding header means the receiver treats the bytes as the body itself. When a client sends compressed bytes without declaring them, the server tries to parse them as the declared media type, and the failure appears as a parse error rather than a transport problem. That is worth knowing because a body that looks corrupted is often correctly compressed and incorrectly declared.
The four codings you will meet
Android clients and their servers negotiate a small set of codings through an accept-encoding request header, and the server answers with content-encoding on the response. Requests can be compressed in the same way.
- Gzip wraps deflate with a header and a trailer. The trailer carries a checksum and a length, which makes truncation easy to detect, and it is the coding most clients fall back to.
- Deflate is ambiguous in practice because two forms exist. One is a zlib stream with a two byte header and a checksum, and the other is a raw deflate stream with neither. A receiver that assumes the wrong one fails on the first bytes.
- Brotli compresses best on the small JSON bodies an API exchanges. It uses a dictionary of common web strings, so a short payload compresses well even when it repeats little.
- Zstd appears on newer clients. It compresses well and decompresses quickly, and the newer codings usually win on small bodies because of dictionary reuse rather than raw ratio.
- Read the header and decode accordingly. Guessing from the body wastes more time than reading the coding, and when deflate fails, trying the other form settles it faster than deciding that the body is not compressed at all.
Whether compression happens before or after signing
The order of these two steps decides which byte sequence the signature covers, and both orders occur in real apps. When the app builds the body, signs it and then compresses it for transport, the signature covers the decoded bytes. When the app compresses first and signs the compressed bytes, the signature covers the encoded form. A third variant signs a canonical form of the fields while sending a compressed serialisation of the same data, so the signature matches neither byte string exactly.
Nothing in the captured bytes says which order the app chose. The signature, the content-encoding header and the body all look the same in a viewer in both cases. What separates them is what your replacement client signs. Signing a decoded body while the server checks the encoded one produces a rejection that names neither compression nor signing.
How a capture tool hides the real body
Most proxies decode a body before showing it, and some rewrite the headers they display. The result is that the readable body on screen is the decoded version, while the bytes on the wire were a compressed stream. A signature computed over the screen version will not match one computed over the wire version, and the difference is invisible in the tool.
Transfer encoding adds another layer. A chunked body in an HTTP/1.1 session has no single content length, and a tool can join the chunks into one body for display. A content-length header that does not match the visible body is a signal that the display has been transformed.
Some clients compress with a coding your library does not support by default, which produces a body that looks like noise in a viewer and decodes correctly on the server. Reading the accept-encoding and content-encoding headers from the app rather than from the tool is the fastest way to settle this.
An order that settles the question
Start from the headers. Read the content-encoding the app sends and the one the server returns, and compare them with what your client sends. Then compare the byte length on the wire with the length you are producing, since a compressible JSON body and its gzip form differ by a factor large enough to spot.
To find out which bytes the signature covers, change a byte inside the body and send the request. A server that recomputes the signature over the decoded body rejects a change that survives decoding. A server that checks the encoded form rejects a change to the compressed stream even when the decoded content still parses. The two experiments take a few minutes and remove the guesswork.
A delivered client handles the codings a standard HTTP library supports and makes the signing order explicit, because that order is the part a library cannot infer. SReverse writes the client in TypeScript or JavaScript with the signing step wired to the right byte sequence, and the same work is delivered as a Python client when that fits the caller's stack.
Related work
Reviewed 28 September 2026 · SReverse research desk