Exchange API WebSocket compression must be decoded at the layer that created it. Let a compliant WebSocket client handle negotiated protocol extensions such as permessage-deflate, then apply any separately documented application compression, such as gzip, to the complete message bytes. Decode the resulting text and parse the expected format only after size and integrity checks pass. Binary delivery alone does not identify a compression codec.
An unreadable payload is not automatically a broken connection. It may be valid compressed data handed to the wrong decoder. Conversely, readable JSON establishes only that parsing worked. This guide follows the bytes from a received message to a validated event, using a small synthetic fixture rather than a live exchange capture.
Identify which layer owns compression
There are two independent contracts to inspect: the WebSocket extension negotiated during the opening handshake and the exchange endpoint's application format.
The protocol extension permessage-deflate uses DEFLATE, is negotiated through Sec-WebSocket-Extensions, and indicates compressed messages with the RSV1 frame bit. It preserves the original text or binary opcode. A compliant implementation processes that extension before delivering the resulting message to the application. These are protocol rules, not a direction to run a gzip decoder in every message callback. See RFC 7692's compression and framing specification.
Application gzip is a separate wrapper around the data the application receives. Enabling protocol decompression does not remove this second wrapper. Equally, turning off a client's protocol extension does not switch off an exchange's application gzip contract.
The published Huobi spot API reference gives a useful scoped contrast: its market WebSocket return data uses gzip, while its Account and Order WebSocket section explicitly says those return data are not gzip compressed. That distinction belongs to the documented endpoints and versions. It is not a rule for every HTX product, another venue or KLINEO's exchange connections. Verify the current endpoint documentation before deploying an adapter.
Binary is a message type, not a codec
At the application boundary, a binary message might contain gzip, another binary serialization, or uncompressed text encoded as bytes. Do not select a decompressor solely because a runtime gives you a buffer.
For example, Node's ws library documents a message callback with both data and isBinary; the boolean identifies the message type. Its message-event API reference is the relevant contract. A buffer's storage type alone does not establish the on-wire opcode.
Record the endpoint, channel, client version, negotiated extensions, callback type and application codec together. If a channel uses JSON text, decode accordingly. If it specifies gzip bytes, retain those bytes until decompression. If it specifies a binary schema, a successful UTF-8 conversion would still be the wrong parser.
Use recognizable byte prefixes as diagnostic evidence, not as authority to override the documented codec. Trying gzip, zlib and raw DEFLATE until one produces something readable can conceal a wrong endpoint or configuration.
A 14-byte fixture through four decoders
The following fictional price message is exactly 14 ASCII bytes, including punctuation: {"p":"100.00"}. It carries no real quote and is not an exchange schema.
Compressing those bytes locally produces the following teaching cases. Sizes describe application payloads only; they exclude WebSocket, TCP and TLS overhead.
| Constructed payload | Payload bytes | Intended application operation | Result |
|---|---|---|---|
| Plain JSON bytes | 14 | UTF-8, then JSON | Object containing price text |
| Gzip member | 34 | Gunzip, then UTF-8 and JSON | Same 14 original bytes |
| Zlib-wrapped DEFLATE | 22 | Zlib decoder, then text and JSON | Same original bytes |
| Raw DEFLATE stream | 16 | Raw DEFLATE decoder, then text and JSON | Same original bytes |
The wrappers differ even though their decoded content agrees. Python's zlib format documentation distinguishes zlib, gzip and raw streams through decoder settings. None of the last two rows is a complete permessage-deflate wire example; that extension has its own framing and state rules.
Reproduce the gzip row with this small fixture. The Python gzip API documents the level and timestamp controls; an explicit level avoids relying on a changing default:
import gzip
raw = b'{"p":"100.00"}'
packed = gzip.compress(
raw,
compresslevel=9,
mtime=0
)
print(len(raw))
print(len(packed))
print(
gzip.decompress(
packed
) == raw
)
The outputs are 14, 34 and True in the checked fixture. Gzip adds 20 bytes here: 34 − 14 = 20. A tiny payload can grow under compression; this example cannot establish a bandwidth saving for a busy feed. Compressor versions can change encoded bytes, so preserve a test vector when exact output matters.
Applying gunzip to the plain, zlib or raw row fails in the checked Python run. Removing the last byte of the gzip row also fails, this time because the member is incomplete. Those are different diagnoses: a wrong format and missing bytes.
Decode complete messages, preserve their order
A network read, a WebSocket frame and an application message are different boundaries. RFC 6455's fragmentation rules allow one message to span multiple frames. Prefer the client's complete-message API; when using a streaming API, follow its reassembly and extension contract before passing data onward.
For the gzip fixture, imagine the same 34 bytes arriving as a 10-byte piece followed by a 24-byte piece. Neither piece is an independent gzip message. Joining them in order restores the original member. Resetting a decoder for every piece would lose necessary state. Do not concatenate two separate complete messages merely because the first failed.
Concurrent decoding introduces another edge case. A large earlier message can finish decompression after a small later message. Preserve the channel's required event order before applying updates. Decompression success does not make an out-of-order book update valid; order book checksum validation is a later, separate check on the resulting state.
Bound expansion before accepting an event
Set both compressed-input and decoded-output limits, along with a processing budget appropriate to the adapter. These are engineering policies to choose and test, not universal exchange defaults. A compressed-input limit cannot by itself bound expanded output.
For a hypothetical policy allowing 64 KiB compressed input and 1 MiB decoded output, a 10 KiB message passes the input check. If it expands beyond 1 MiB, stop at the output boundary and reject it. Allocating the entire result and checking its size afterward defeats the intended memory bound.
Streaming decoders also need completion checks. Python's decompression-object documentation distinguishes end-of-stream, unconsumed input and trailing data, and provides an output cap per call. An adapter must still enforce its cumulative budget and its contract for extra members or trailing bytes.
After decompression, validate character encoding, parse the expected format, classify the event and check its fields. Preserve identifier precision during parsing; the large integer identifier guide follows the next failure boundary. A decoded identifier that has already been rounded cannot be repaired by converting it to a string afterward.
Keep a failure-stage worksheet
| Observation | Inspect next | Acceptance decision |
|---|---|---|
| Protocol reports an unexpected compression bit | Negotiated extension and client settings | Do not guess an application codec |
| Gzip rejects readable JSON | Endpoint's application format | Correct the codec contract |
| Gzip reports incomplete input | Message boundary and retained byte count | Reject the partial event |
| Output crosses the configured cap | Cumulative expansion budget | Stop decoding and reject |
| JSON parses but says subscription failed | Application event and error fields | Do not mark the channel ready |
| Payload validates but book continuity is uncertain | Sequence and state validation | Keep derived state unverified |
The fifth row resembles HTTP 200 error envelopes: successful delivery and valid syntax are distinct from application success. The HTTP status rule itself does not apply to subsequent WebSocket messages; the shared lesson is to inspect the application result.
Keep redacted diagnostic records: endpoint and channel, receive order, lengths at each stage, selected codec, completion status and failure stage. Avoid dumping private account payloads into logs. Attach an observation time to the last validated event; connection health depends on the scope and age of the evidence.
A rejected message leaves a data-quality question even if the socket remains open. Use the WebSocket recovery guide for the separate continuity procedure. This decoding worksheet establishes which bytes became an accepted event; it does not prove that every required event arrived.
Prepared with AI assistance on 23 October 2026 under the editorial policy. Protocol standards, official exchange reference material and library documentation informed this guide. The byte fixture and limits are educational constructions checked locally; no live feed or trading integration was tested.
