Skip to content

Commit 03de960

Browse files
authored
gzhttp: Add zstandard to server handler wrapper (#1121)
Both gzip and zstd compression are now enabled by default. When a client supports both, zstd is preferred due to its better compression ratio and speed. Zstd compression is enabled by default alongside gzip. When the client supports both, zstd is preferred because it typically offers better compression ratios and faster decompression. The server uses `Accept-Encoding` header negotiation to select the best encoding: - If client only accepts `gzip` → response is gzip compressed - If client only accepts `zstd` → response is zstd compressed - If client accepts both with equal qvalues → zstd is used (configurable) - If client specifies qvalues (e.g., `gzip;q=1.0, zstd;q=0.5`) → higher qvalue wins Default zstd settings are conservative for broad compatibility: - Level: `SpeedFastest` (1) - maximum speed - Window size: 128KB - minimal memory usage - Concurrency: 1 - single-threaded per request
1 parent bb1ab3b commit 03de960

8 files changed

Lines changed: 905 additions & 60 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This package provides various compression algorithms.
77
* Optimized [deflate](https://godoc.org/github.com/klauspost/compress/flate) packages which can be used as a dropin replacement for [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip) and [zlib](https://godoc.org/github.com/klauspost/compress/zlib).
88
* [snappy](https://github.com/klauspost/compress/tree/master/snappy) is a drop-in replacement for `github.com/golang/snappy` offering better compression and concurrent streams.
99
* [huff0](https://github.com/klauspost/compress/tree/master/huff0) and [FSE](https://github.com/klauspost/compress/tree/master/fse) implementations for raw entropy encoding.
10-
* [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped requests efficiently.
10+
* [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped/zstd HTTP requests efficiently.
1111
* [pgzip](https://github.com/klauspost/pgzip) is a separate package that provides a very fast parallel gzip implementation.
1212

1313
[![Go Reference](https://pkg.go.dev/badge/klauspost/compress.svg)](https://pkg.go.dev/github.com/klauspost/compress?tab=subdirectories)

gzhttp/README.md

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
Gzip Middleware
2-
===============
1+
Gzip/Zstd HTTP Middleware
2+
====================
33

4-
This Go package which wraps HTTP *server* handlers to transparently gzip the
5-
response body, for clients which support it.
4+
This Go package wraps HTTP *server* handlers to transparently compress the
5+
response body using gzip or zstd, for clients which support it.
66

7-
For HTTP *clients* we provide a transport wrapper that will do gzip decompression
7+
Both gzip and zstd compression are enabled by default. When a client supports both,
8+
zstd is preferred due to its better compression ratio and speed.
9+
10+
For HTTP *clients* we provide a transport wrapper that will do gzip/zstd decompression
811
faster than what the standard library offers.
912

1013
Both the client and server wrappers are fully compatible with other servers and clients.
@@ -134,10 +137,72 @@ func main() {
134137

135138
```
136139

140+
### Zstd Compression
141+
142+
Zstd compression is enabled by default alongside gzip. When the client supports both,
143+
zstd is preferred because it typically offers better compression ratios and faster decompression.
144+
145+
The server uses `Accept-Encoding` header negotiation to select the best encoding:
146+
- If client only accepts `gzip` → response is gzip compressed
147+
- If client only accepts `zstd` → response is zstd compressed
148+
- If client accepts both with equal qvalues → zstd is used (configurable)
149+
- If client specifies qvalues (e.g., `gzip;q=1.0, zstd;q=0.5`) → higher qvalue wins
150+
151+
#### Zstd Options
152+
153+
```Go
154+
// Disable zstd, only use gzip
155+
wrapper, _ := gzhttp.NewWrapper(gzhttp.EnableZstd(false))
156+
157+
// Disable gzip, only use zstd
158+
wrapper, _ := gzhttp.NewWrapper(gzhttp.EnableGzip(false))
159+
160+
// Prefer gzip when client accepts both with equal qvalues
161+
wrapper, _ := gzhttp.NewWrapper(gzhttp.PreferZstd(false))
162+
163+
// Set zstd compression level (1=fastest, 2=default, 3=better, 4=best)
164+
wrapper, _ := gzhttp.NewWrapper(gzhttp.ZstdCompressionLevel(int(zstd.SpeedDefault)))
165+
166+
// Use custom zstd writer implementation
167+
wrapper, _ := gzhttp.NewWrapper(gzhttp.ZstdImplementation(myZstdFactory))
168+
```
169+
170+
Default zstd settings are conservative for broad compatibility:
171+
- Level: `SpeedFastest` (1) - maximum speed
172+
- Window size: 128KB - minimal memory usage
173+
- Concurrency: 1 - single-threaded per request
137174

138175
### Performance
139176

140-
Speed compared to [nytimes/gziphandler](https://github.com/nytimes/gziphandler) with default settings, 2KB, 20KB and 100KB:
177+
#### Gzip vs Zstd
178+
179+
Zstd is significantly faster than gzip at default settings while providing similar or better compression:
180+
181+
```
182+
Single-threaded performance (2KB, 20KB, 100KB payload):
183+
BenchmarkGzipHandler_S2k 137.22 MB/s 3532 B/op 15 allocs/op
184+
BenchmarkZstdHandler_S2k 219.21 MB/s 1936 B/op 12 allocs/op (1.6x faster, 45% less memory)
185+
186+
BenchmarkGzipHandler_S20k 306.91 MB/s 18616 B/op 18 allocs/op
187+
BenchmarkZstdHandler_S20k 434.05 MB/s 7595 B/op 12 allocs/op (1.4x faster, 59% less memory)
188+
189+
BenchmarkGzipHandler_S100k 198.96 MB/s 66937 B/op 20 allocs/op
190+
BenchmarkZstdHandler_S100k 368.70 MB/s 63021 B/op 16 allocs/op (1.9x faster)
191+
192+
Parallel performance:
193+
BenchmarkGzipHandler_P2k 997.01 MB/s 3148 B/op 15 allocs/op
194+
BenchmarkZstdHandler_P2k 1440.12 MB/s 1985 B/op 12 allocs/op (1.4x faster)
195+
196+
BenchmarkGzipHandler_P20k 2129.70 MB/s 17572 B/op 18 allocs/op
197+
BenchmarkZstdHandler_P20k 2928.82 MB/s 7498 B/op 12 allocs/op (1.4x faster)
198+
199+
BenchmarkGzipHandler_P100k 1678.72 MB/s 67316 B/op 20 allocs/op
200+
BenchmarkZstdHandler_P100k 2392.23 MB/s 61122 B/op 16 allocs/op (1.4x faster)
201+
```
202+
203+
#### Comparison to nytimes/gziphandler
204+
205+
Speed compared to [nytimes/gziphandler](https://github.com/nytimes/gziphandler) with default settings, 2KB, 20KB and 100KB:
141206

142207
```
143208
λ benchcmp before.txt after.txt
@@ -223,32 +288,37 @@ size can reveal if there are overlaps between the secret data and the injected d
223288

224289
For more information see https://breachattack.com/
225290

226-
It can be hard to judge if you are vulnerable to BREACH.
291+
It can be hard to judge if you are vulnerable to BREACH.
227292
In general, if you do not include any user provided content in the response body you are safe,
228293
but if you do, or you are in doubt, you can apply mitigations.
229294

230295
`gzhttp` can apply [Heal the Breach](https://ieeexplore.ieee.org/document/9754554), or improved content aware padding.
231296

297+
RandomJitter works with both gzip and zstd compression:
298+
- **gzip**: Jitter is added as a Comment field in the gzip header
299+
- **zstd**: Jitter is added as a skippable frame (RFC 8878 Section 3.1.2) after the compressed data
300+
232301
```Go
233302
// RandomJitter adds 1->n random bytes to output based on checksum of payload.
234303
// Specify the amount of input to buffer before applying jitter.
235304
// This should cover the sensitive part of your response.
236305
// This can be used to obfuscate the exact compressed size.
237306
// Specifying 0 will use a buffer size of 64KB.
238-
// 'paranoid' will use a slower hashing function, that MAY provide more safety.
307+
// 'paranoid' will use a slower hashing function, that MAY provide more safety.
239308
// If a negative buffer is given, the amount of jitter will not be content dependent.
240309
// This provides *less* security than applying content based jitter.
241310
func RandomJitter(n, buffer int, paranoid bool) option
242-
...
311+
...
243312
```
244313

245-
The jitter is added as a "Comment" field. This field has a 1 byte overhead, so actual extra size will be 2 -> n+1 (inclusive).
314+
For gzip, the jitter is added as a "Comment" field with 1 byte overhead (actual extra size: 2 -> n+1 inclusive).
315+
For zstd, the jitter is added as a skippable frame with 8 byte overhead (actual extra size: 9 -> n+8 inclusive).
246316

247317
A good option would be to apply 32 random bytes, with default 64KB buffer: `gzhttp.RandomJitter(32, 0, false)`.
248318

249319
Note that flushing the data forces the padding to be applied, which means that only data before the flush is considered for content aware padding.
250320

251-
The *padding* in the comment is the text `Padding-Padding-Padding-Padding-Pad....`
321+
The *padding* content is the text `Padding-Padding-Padding-Padding-Pad....`
252322

253323
The *length* is `1 + crc32c(payload) MOD n` or `1 + sha256(payload) MOD n` (paranoid), or just random from `crypto/rand` if buffer < 0.
254324

0 commit comments

Comments
 (0)