|
1 | | -Gzip Middleware |
2 | | -=============== |
| 1 | +Gzip/Zstd HTTP Middleware |
| 2 | +==================== |
3 | 3 |
|
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. |
6 | 6 |
|
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 |
8 | 11 | faster than what the standard library offers. |
9 | 12 |
|
10 | 13 | Both the client and server wrappers are fully compatible with other servers and clients. |
@@ -134,10 +137,72 @@ func main() { |
134 | 137 |
|
135 | 138 | ``` |
136 | 139 |
|
| 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 |
137 | 174 |
|
138 | 175 | ### Performance |
139 | 176 |
|
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: |
141 | 206 |
|
142 | 207 | ``` |
143 | 208 | λ benchcmp before.txt after.txt |
@@ -223,32 +288,37 @@ size can reveal if there are overlaps between the secret data and the injected d |
223 | 288 |
|
224 | 289 | For more information see https://breachattack.com/ |
225 | 290 |
|
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. |
227 | 292 | In general, if you do not include any user provided content in the response body you are safe, |
228 | 293 | but if you do, or you are in doubt, you can apply mitigations. |
229 | 294 |
|
230 | 295 | `gzhttp` can apply [Heal the Breach](https://ieeexplore.ieee.org/document/9754554), or improved content aware padding. |
231 | 296 |
|
| 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 | + |
232 | 301 | ```Go |
233 | 302 | // RandomJitter adds 1->n random bytes to output based on checksum of payload. |
234 | 303 | // Specify the amount of input to buffer before applying jitter. |
235 | 304 | // This should cover the sensitive part of your response. |
236 | 305 | // This can be used to obfuscate the exact compressed size. |
237 | 306 | // 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. |
239 | 308 | // If a negative buffer is given, the amount of jitter will not be content dependent. |
240 | 309 | // This provides *less* security than applying content based jitter. |
241 | 310 | func RandomJitter(n, buffer int, paranoid bool) option |
242 | | -... |
| 311 | +... |
243 | 312 | ``` |
244 | 313 |
|
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). |
246 | 316 |
|
247 | 317 | A good option would be to apply 32 random bytes, with default 64KB buffer: `gzhttp.RandomJitter(32, 0, false)`. |
248 | 318 |
|
249 | 319 | 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. |
250 | 320 |
|
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....` |
252 | 322 |
|
253 | 323 | The *length* is `1 + crc32c(payload) MOD n` or `1 + sha256(payload) MOD n` (paranoid), or just random from `crypto/rand` if buffer < 0. |
254 | 324 |
|
|
0 commit comments