Skip to content

gzhttp: Add zstandard to server handler wrapper#1121

Merged
klauspost merged 4 commits intomasterfrom
gzhttp-zstd
Jan 14, 2026
Merged

gzhttp: Add zstandard to server handler wrapper#1121
klauspost merged 4 commits intomasterfrom
gzhttp-zstd

Conversation

@klauspost
Copy link
Owner

@klauspost klauspost commented Jan 13, 2026

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

Summary by CodeRabbit

  • New Features

    • Adds zstd compression alongside gzip with enable/disable, preference, and compression-level options; server negotiates encoding via Accept-Encoding/q-values.
    • Adds a zstd-backed writer implementation and pooled encoder strategy for improved performance.
  • Security

    • BREACH-mitigation jitter extended to work with both gzip and zstd (header/comment and skippable-frame approaches).
  • Documentation

    • README expanded with zstd options, negotiation rules, performance notes, and jitter examples.
  • Tests

    • Comprehensive zstd tests and benchmarks added.

✏️ Tip: You can customize this high-level summary in your review settings.

Both gzip and zstd compression are 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
@klauspost klauspost requested a review from Copilot January 13, 2026 17:12
@coderabbitai

This comment was marked as resolved.

@klauspost klauspost changed the title Add zstandard to server handler wrapper gzhttp: Add zstandard to server handler wrapper Jan 13, 2026

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
gzhttp/compress.go (1)

257-261: ETag suffix replacement may have no effect for custom suffixes.

The strings.Replace only transforms the suffix if it contains "gzip". If a user specifies a custom suffix without "gzip" (e.g., SuffixETag("-compressed")), both gzip and zstd responses will use the same ETag suffix, potentially causing cache conflicts.

Consider documenting this behavior or providing an encoding-specific suffix option.

📝 Possible alternative approach
 		suffix := w.suffixETag
 		if w.enc == encodingZstd {
-			suffix = strings.Replace(suffix, "gzip", "zstd", 1)
+			// If suffix contains "gzip", replace it; otherwise append encoding indicator
+			if strings.Contains(suffix, "gzip") {
+				suffix = strings.Replace(suffix, "gzip", "zstd", 1)
+			} else if !strings.Contains(suffix, "zstd") {
+				suffix = suffix + "-zstd"
+			}
 		}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0aedf0 and 1c50f8e.

📒 Files selected for processing (5)
  • README.md
  • gzhttp/compress.go
  • gzhttp/writer/gzkp/gzkp.go
  • gzhttp/writer/interface.go
  • gzhttp/writer/zstdkp/zstdkp.go
✅ Files skipped from review due to trivial changes (1)
  • gzhttp/writer/gzkp/gzkp.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • gzhttp/writer/zstdkp/zstdkp.go
🧰 Additional context used
🧬 Code graph analysis (1)
gzhttp/writer/interface.go (4)
gzhttp/writer/gzkp/gzkp.go (1)
  • Levels (78-80)
gzhttp/writer/zstdkp/zstdkp.go (1)
  • Levels (74-76)
gzhttp/writer/gzstd/stdlib.go (1)
  • Levels (79-81)
zstd/internal/xxhash/xxhash.go (1)
  • New (39-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (16)
  • GitHub Check: build (1.23.x, windows-latest)
  • GitHub Check: build (1.25.x, macos-latest)
  • GitHub Check: build (1.25.x, windows-latest)
  • GitHub Check: build (1.23.x, ubuntu-latest)
  • GitHub Check: build (1.24.x, windows-latest)
  • GitHub Check: build (1.23.x, macos-latest)
  • GitHub Check: build (1.25.x, ubuntu-latest)
  • GitHub Check: build (1.24.x, macos-latest)
  • GitHub Check: build-special
  • GitHub Check: build (1.24.x, ubuntu-latest)
  • GitHub Check: fuzz-other (nounsafe)
  • GitHub Check: fuzz-s2 (nounsafe)
  • GitHub Check: fuzz-other ("noasm,nounsafe")
  • GitHub Check: fuzz-s2 ("noasm,nounsafe")
  • GitHub Check: fuzz-zstd (nounsafe)
  • GitHub Check: fuzz-zstd ("noasm,nounsafe")
🔇 Additional comments (13)
README.md (1)

10-10: LGTM!

Documentation accurately reflects the new dual compression support (gzip and zstd).

gzhttp/writer/interface.go (1)

46-61: LGTM!

The ZstdWriter and ZstdWriterFactory interfaces follow the same pattern as the existing gzip equivalents, providing a clean and consistent API for pluggable zstd implementations. The omission of a ZstdWriterExt equivalent is correct since zstd uses skippable frames (not headers) for jitter.

gzhttp/compress.go (11)

86-93: LGTM!

The encoding enum is well-designed with encodingNone as the zero value, ensuring safe defaults.


105-111: LGTM!

Zstd fields in GzipResponseWriter are well-structured, mirroring the gzip pattern and properly supporting jitter via skippable frames.


137-144: LGTM!

The Write method correctly routes to the appropriate compression writer (gzip or zstd) based on which is initialized.


469-484: LGTM - RFC 8878 compliant skippable frame implementation.

The skippable frame format is correct per RFC 8878 Section 3.1.2. The magic number 0x184D2A50 is within the valid range (0x184D2A5X). The jitter size is validated in config.validate() (line 731-732) to be less than math.MaxUint16, so the uint32 cast at line 478 is safe.


706-744: LGTM!

Validation logic is comprehensive: it validates compression levels only when the respective encoding is enabled, ensures at least one encoding is active, and correctly limits jitter header validation to gzip (since zstd uses skippable frames instead).


994-1040: LGTM!

The selectEncoding function correctly implements RFC 2616 content negotiation with proper handling of edge cases: HEAD requests, missing headers, single encoding availability, and qvalue comparison with configurable preference tiebreaker.


1042-1060: LGTM!

Clean implementation that correctly parses qvalues using the existing parseCoding helper, returning 0 when the encoding is not present (distinct from the default qvalue of 1.0 when present without explicit q=).


790-830: LGTM!

New configuration options follow the existing functional options pattern with clear documentation. Default settings (both encodings enabled, zstd preferred, fastest compression level) are sensible for broad compatibility as stated in PR objectives.


486-538: LGTM!

The Flush method correctly handles both compression encodings, flushing the appropriate writer when active.


581-591: LGTM!

Default configuration aligns with PR objectives: both encodings enabled, zstd preferred, conservative SpeedFastest level for compatibility.


1-27: LGTM!

Comprehensive package documentation clearly explains dual compression support, content negotiation behavior, and usage patterns.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @gzhttp/compress.go:
- Around line 531-534: The call to w.startCompression(nil) inside Flush ignores
its returned error; update the Flush implementation to capture the error from
startCompression, and if non-nil either log the error (using the existing logger
on w or fmt.Errorf if no logger exists) and then call w.startPlain(), or
otherwise fall back to startPlain() when startCompression fails; ensure you
still satisfy the http.Flusher behavior by handling the error path in the same
Flush method.
🧹 Nitpick comments (1)
gzhttp/compress.go (1)

1051-1069: Consider unifying with parseEncodingGzip.

This function is nearly identical to parseEncodingGzip (lines 1093-1112), with the only difference being the parameterized encoding name. Consider consolidating them to reduce code duplication.

Suggested refactor

The existing parseEncodingGzip could be replaced with calls to parseEncodingQValue:

 // parseEncodingGzip returns the qvalue of gzip compression.
 func parseEncodingGzip(s string) float64 {
-	s = strings.TrimSpace(s)
-
-	for len(s) > 0 {
-		stop := strings.IndexByte(s, ',')
-		if stop < 0 {
-			stop = len(s)
-		}
-		coding, qvalue, _ := parseCoding(s[:stop])
-
-		if coding == "gzip" {
-			return qvalue
-		}
-		if stop == len(s) {
-			break
-		}
-		s = s[stop+1:]
-	}
-	return 0
+	return parseEncodingQValue(s, "gzip")
 }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c50f8e and 012d223.

📒 Files selected for processing (2)
  • gzhttp/compress.go
  • gzhttp/compress_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • gzhttp/compress_test.go
🧰 Additional context used
🧬 Code graph analysis (1)
gzhttp/compress.go (4)
gzhttp/writer/interface.go (4)
  • ZstdWriterFactory (54-61)
  • ZstdWriter (47-51)
  • Header (28-34)
  • GzipWriterExt (20-25)
gzhttp/writer/gzkp/gzkp.go (2)
  • Levels (78-80)
  • NewWriter (59-67)
gzhttp/writer/zstdkp/zstdkp.go (2)
  • Levels (74-76)
  • NewWriter (63-71)
gzhttp/writer/gzstd/stdlib.go (2)
  • Levels (79-81)
  • NewWriter (60-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: build (1.25.x, macos-latest)
  • GitHub Check: build (1.24.x, ubuntu-latest)
  • GitHub Check: build (1.24.x, windows-latest)
  • GitHub Check: build (1.23.x, macos-latest)
  • GitHub Check: build (1.23.x, ubuntu-latest)
  • GitHub Check: build (1.25.x, ubuntu-latest)
  • GitHub Check: fuzz-zstd ("noasm,nounsafe")
  • GitHub Check: fuzz-other ("noasm,nounsafe")
  • GitHub Check: fuzz-s2 ("noasm,nounsafe")
  • GitHub Check: build-special
  • GitHub Check: fuzz-other (nounsafe)
🔇 Additional comments (10)
gzhttp/compress.go (10)

1-52: LGTM!

The package documentation clearly explains the dual-encoding support and content negotiation behavior. Imports are appropriate for the new zstd functionality.


86-126: LGTM!

The encoding enum and struct extensions are well-designed. The zstd fields appropriately mirror their gzip counterparts, and the zstdJitter field for skippable frame support is a clean approach.


137-220: LGTM!

The Write method correctly routes data to either the gzip or zstd writer based on which is active. The ordering of checks is consistent throughout.


228-270: LGTM!

The startCompression method handles encoding-specific header setup correctly. The ETag suffix transformation logic properly differentiates between gzip and zstd responses to avoid cache conflicts.


478-493: LGTM!

The zstd skippable frame implementation correctly follows RFC 8878 Section 3.1.2. The magic number 0x184D2A50 is within the valid range, and the frame format (magic + size + data) is correctly structured. Writing the skippable frame after zw.Close() ensures the main zstd frame is complete before appending jitter.


580-661: LGTM!

The wrapper initialization correctly sets conservative zstd defaults (SpeedFastest level, both encodings enabled, zstd preferred). The GzipResponseWriter is properly initialized with all encoding-related fields.


715-752: LGTM!

The validation logic is thorough. It correctly validates compression levels conditionally based on which encodings are enabled, ensures at least one encoding is active, and only validates gzip jitter support when gzip is enabled (zstd jitter uses skippable frames that don't require special writer support).


799-839: LGTM!

The new option functions follow the established pattern and provide clear documentation. The EnableGzip option enables zstd-only configurations, which is a nice flexibility.


1003-1049: LGTM!

The selectEncoding function correctly implements RFC-compliant content negotiation. It properly handles the HEAD request edge case, respects qvalue comparisons, and uses the configurable preference when qvalues are equal.


424-476: LGTM!

The Close method correctly handles both writer types, properly closes and nils them, and writes the zstd skippable frame only when jitter data is present. The early return on zstd close error prevents writing a skippable frame to a failed stream.

@klauspost klauspost merged commit 03de960 into master Jan 14, 2026
22 checks passed
@klauspost klauspost deleted the gzhttp-zstd branch January 14, 2026 09:41
felixbuenemann added a commit to felixbuenemann/thruster that referenced this pull request Feb 18, 2026
The new version enabled both gzip and zstd compression support in
gzhttp, with auto-negotiation and preference for zstd. See:

klauspost/compress#1121

Also added a tests to check zstd compression and preference.
felixbuenemann added a commit to felixbuenemann/thruster that referenced this pull request Feb 18, 2026
The new version enabled both gzip and zstd compression support in
gzhttp, with auto-negotiation and preference for zstd. See:

klauspost/compress#1121

Also added tests to check zstd compression and preference.
felixbuenemann added a commit to felixbuenemann/thruster that referenced this pull request Feb 18, 2026
The new version enables both gzip and zstd compression support in
gzhttp, with auto-negotiation and preference for zstd. See:

klauspost/compress#1121

Also added tests to check zstd compression and preference.
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