Repository: valyala/fasthttp
Pull request #1950 (2025-02-09 18:21:25)

fix: compression priority
Before this PR, fasthttp would never use zstd if clients sent their `Accept-Encoding` header including gzip because zstd was the last option and would only be used if zstd was the only encoding available in `Accept-Encoding` header. This is really an issue when serving precompressed sidecars of zstd and gzip where the server preferred the latter.

The following order has been established; first brotli, then zstd, and finally gzip. It takes into account whether `CompressBrotli` and the new `CompressZstd` option are enabled to select which compression to use.

It also fixes the missing Vary header on compressed assets.

~~There is also an issue that has been revealed with the rework of these tests that should be addressed in a separate PR~~


Patch:
diff --git a/.gitignore b/.gitignore
index 7673684..27425e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,8 @@
 tags
 *.pprof
-*.fasthttp.gz
 *.fasthttp.br
+*.fasthttp.zst
+*.fasthttp.gz
 .idea
 .vscode
 .DS_Store
diff --git a/fs.go b/fs.go
index 22f8217..784d49b 100644
--- a/fs.go
+++ b/fs.go
@@ -133,6 +133,7 @@ var (
 		GenerateIndexPages: true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CompressZstd:       true,
 		AcceptByteRange:    true,
 	}
 	rootFSHandler RequestHandler
@@ -156,6 +157,7 @@ func ServeFS(ctx *RequestCtx, filesystem fs.FS, path string) {
 		GenerateIndexPages: true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CompressZstd:       true,
 		AcceptByteRange:    true,
 	}
 	handler := f.NewRequestHandler()
@@ -321,13 +323,20 @@ type FS struct {
 	// absolute paths on any filesystem.
 	AllowEmptyRoot bool
 
-	// Uses brotli encoding and fallbacks to gzip in responses if set to true, uses gzip if set to false.
+	// Uses brotli encoding and fallbacks to zstd or gzip in responses if set to true, uses zstd or gzip if set to false.
 	//
 	// This value has sense only if Compress is set.
 	//
 	// Brotli encoding is disabled by default.
 	CompressBrotli bool
 
+	// Uses zstd encoding and fallbacks to gzip in responses if set to true, uses gzip if set to false.
+	//
+	// This value has sense only if Compress is set.
+	//
+	// zstd encoding is disabled by default.
+	CompressZstd bool
+
 	// Index pages for directories without files matching IndexNames
 	// are automatically generated if set.
 	//
@@ -487,6 +496,7 @@ func (fs *FS) initRequestHandler() {
 		generateIndexPages:     fs.GenerateIndexPages,
 		compress:               fs.Compress,
 		compressBrotli:         fs.CompressBrotli,
+		compressZstd:           fs.CompressZstd,
 		compressRoot:           compressRoot,
 		pathNotFound:           fs.PathNotFound,
 		acceptByteRange:        fs.AcceptByteRange,
@@ -518,6 +528,7 @@ type fsHandler struct {
 	generateIndexPages bool
 	compress           bool
 	compressBrotli     bool
+	compressZstd       bool
 	acceptByteRange    bool
 }
 
@@ -1049,14 +1060,14 @@ func (h *fsHandler) handleRequest(ctx *RequestCtx) {
 			mustCompress = true
 			fileCacheKind = brotliCacheKind
 			fileEncoding = "br"
+		case h.compressZstd && ctx.Request.Header.HasAcceptEncodingBytes(strZstd):
+			mustCompress = true
+			fileCacheKind = zstdCacheKind
+			fileEncoding = "zstd"
 		case ctx.Request.Header.HasAcceptEncodingBytes(strGzip):
 			mustCompress = true
 			fileCacheKind = gzipCacheKind
 			fileEncoding = "gzip"
-		case ctx.Request.Header.HasAcceptEncodingBytes(strZstd):
-			mustCompress = true
-			fileCacheKind = zstdCacheKind
-			fileEncoding = "zstd"
 		}
 	}
 
@@ -1122,10 +1133,13 @@ func (h *fsHandler) handleRequest(ctx *RequestCtx) {
 		switch fileEncoding {
 		case "br":
 			hdr.SetContentEncodingBytes(strBr)
+			hdr.addVaryBytes(strAcceptEncoding)
 		case "gzip":
 			hdr.SetContentEncodingBytes(strGzip)
+			hdr.addVaryBytes(strAcceptEncoding)
 		case "zstd":
 			hdr.SetContentEncodingBytes(strZstd)
+			hdr.addVaryBytes(strAcceptEncoding)
 		}
 	}
 
diff --git a/go.mod b/go.mod
index 4d9df2c..00c4e97 100644
--- a/go.mod
+++ b/go.mod
@@ -6,9 +6,9 @@ require (
 	github.com/andybalholm/brotli v1.1.1
 	github.com/klauspost/compress v1.17.11
 	github.com/valyala/bytebufferpool v1.0.0
-	golang.org/x/crypto v0.32.0
-	golang.org/x/net v0.34.0
+	golang.org/x/crypto v0.33.0
+	golang.org/x/net v0.35.0
 	golang.org/x/sys v0.30.0
 )
 
-require golang.org/x/text v0.21.0 // indirect
+require golang.org/x/text v0.22.0 // indirect
diff --git a/go.sum b/go.sum
index 58ed84d..6a0394d 100644
--- a/go.sum
+++ b/go.sum
@@ -6,11 +6,11 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
-golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
-golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
-golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
 golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
 golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=


Test patch:
diff --git a/fs_fs_test.go b/fs_fs_test.go
index 9ff1e2d..dd767d8 100644
--- a/fs_fs_test.go
+++ b/fs_fs_test.go
@@ -61,38 +61,80 @@ func TestFSServeFileCompressed(t *testing.T) {
 
 	var resp Response
 
-	// request compressed gzip file
+	expectedBody, err := getFileContents("/fs.go")
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	// should prefer brotli over zstd, gzip and ignore unknown encoding
 	ctx.Request.SetRequestURI("http://foobar.com/baz")
-	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
 	ServeFS(&ctx, fsTestFilesystem, "fs.go")
 
 	s := ctx.Response.String()
 	br := bufio.NewReader(bytes.NewBufferString(s))
-	if err := resp.Read(br); err != nil {
+	if err = resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
+	}
 
 	ce := resp.Header.ContentEncoding()
-	if string(ce) != "gzip" {
-		t.Fatalf("Unexpected 'Content-Encoding' %q. Expecting %q", ce, "gzip")
+	if string(ce) != "br" {
+		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "br")
+	}
+
+	vary := resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
 	}
 
-	body, err := resp.BodyGunzip()
+	body, err := resp.BodyUnbrotli()
 	if err != nil {
+		t.Fatalf("unexpected error on unbrotli response body: %v", err)
+	}
+	if !bytes.Equal(body, expectedBody) {
+		t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
+	}
+
+	// should prefer zstd over gzip and ignore unknown encoding
+	ctx.Request.Reset()
+	ctx.Request.SetRequestURI("http://foobar.com/baz")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
+	ServeFS(&ctx, fsTestFilesystem, "fs.go")
+
+	s = ctx.Response.String()
+	br = bufio.NewReader(bytes.NewBufferString(s))
+	if err = resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
-	expectedBody, err := getFileContents("/fs.go")
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
+	}
+
+	ce = resp.Header.ContentEncoding()
+	if string(ce) != "zstd" {
+		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "zstd")
+	}
+
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
+	}
+
+	body, err = resp.BodyUnzstd()
 	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
+		t.Fatalf("unexpected error on unzstd response body: %v", err)
 	}
 	if !bytes.Equal(body, expectedBody) {
-		t.Fatalf("unexpected body %q. expecting %q", body, expectedBody)
+		t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
 	}
 
-	// request compressed brotli file
+	// should prefer gzip and ignore unknown encoding
 	ctx.Request.Reset()
 	ctx.Request.SetRequestURI("http://foobar.com/baz")
-	ctx.Request.Header.Set(HeaderAcceptEncoding, "br")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
 	ServeFS(&ctx, fsTestFilesystem, "fs.go")
 
 	s = ctx.Response.String()
@@ -100,22 +142,68 @@ func TestFSServeFileCompressed(t *testing.T) {
 	if err = resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
+	}
 
 	ce = resp.Header.ContentEncoding()
-	if string(ce) != "br" {
-		t.Fatalf("Unexpected 'Content-Encoding' %q. Expecting %q", ce, "br")
+	if string(ce) != "gzip" {
+		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "gzip")
+	}
+
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
 	}
 
-	body, err = resp.BodyUnbrotli()
+	body, err = resp.BodyGunzip()
 	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
+		t.Fatalf("unexpected error on gunzip response body: %v", err)
+	}
+	if !bytes.Equal(body, expectedBody) {
+		t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
 	}
-	expectedBody, err = getFileContents("/fs.go")
+}
+
+func TestFSServeFileUncompressed(t *testing.T) {
+	t.Parallel()
+
+	var ctx RequestCtx
+	ctx.Init(&Request{}, nil, nil)
+
+	var resp Response
+
+	expectedBody, err := getFileContents("/fs.go")
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
+
+	ctx.Request.SetRequestURI("http://foobar.com/baz")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
+	ServeFileUncompressed(&ctx, "fs.go")
+
+	s := ctx.Response.String()
+	br := bufio.NewReader(bytes.NewBufferString(s))
+	if err = resp.Read(br); err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
+	}
+
+	ce := resp.Header.ContentEncoding()
+	if len(ce) > 0 {
+		t.Fatalf("unexpected 'Content-Encoding': %q. Expecting \"\"", string(ce))
+	}
+
+	vary := resp.Header.PeekBytes(strVary)
+	if len(vary) > 0 {
+		t.Fatalf("unexpected 'Vary': %q. Expecting \"\"", string(vary))
+	}
+
+	body := resp.Body()
 	if !bytes.Equal(body, expectedBody) {
-		t.Fatalf("unexpected body %q. expecting %q", body, expectedBody)
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
 	}
 }
 
@@ -188,6 +276,7 @@ func TestFSFSCompressConcurrent(t *testing.T) {
 		GenerateIndexPages: true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CompressZstd:       true,
 		CleanStop:          stop,
 	}
 	h := fs.NewRequestHandler()
@@ -226,6 +315,7 @@ func TestFSFSCompressSingleThread(t *testing.T) {
 		GenerateIndexPages: true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CompressZstd:       true,
 		CleanStop:          stop,
 	}
 	h := fs.NewRequestHandler()
@@ -241,72 +331,128 @@ func testFSFSCompress(t *testing.T, h RequestHandler, filePath string) {
 
 	var resp Response
 
-	// request uncompressed file
-	ctx.Request.Reset()
+	// get uncompressed
 	ctx.Request.SetRequestURI(filePath)
 	h(&ctx)
+
 	s := ctx.Response.String()
 	br := bufio.NewReader(bytes.NewBufferString(s))
 	if err := resp.Read(br); err != nil {
-		t.Errorf("unexpected error: %v. filePath=%q", err, filePath)
+		t.Fatalf("unexpected error: %v", err)
 	}
 	if resp.StatusCode() != StatusOK {
-		t.Errorf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
 	}
+
 	ce := resp.Header.ContentEncoding()
-	if len(ce) != 0 {
-		t.Errorf("unexpected content-encoding %q. Expecting empty string. filePath=%q", ce, filePath)
+	if len(ce) > 0 {
+		t.Fatalf("unexpected 'Content-Encoding': %q. Expecting \"\"", string(ce))
+	}
+
+	vary := resp.Header.PeekBytes(strVary)
+	if len(vary) > 0 {
+		t.Fatalf("unexpected 'Vary': %q. Expecting \"\"", string(vary))
 	}
-	body := string(resp.Body())
 
-	// request compressed gzip file
+	expectedBody := bytes.Clone(resp.Body())
+
+	// should prefer brotli over zstd, gzip and ignore unknown encoding
 	ctx.Request.Reset()
 	ctx.Request.SetRequestURI(filePath)
-	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
 	h(&ctx)
+
 	s = ctx.Response.String()
 	br = bufio.NewReader(bytes.NewBufferString(s))
 	if err := resp.Read(br); err != nil {
-		t.Errorf("unexpected error: %v. filePath=%q", err, filePath)
+		t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
 	}
 	if resp.StatusCode() != StatusOK {
-		t.Errorf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
+		t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
 	}
+
 	ce = resp.Header.ContentEncoding()
-	if string(ce) != "gzip" {
-		t.Errorf("unexpected content-encoding %q. Expecting %q. filePath=%q", ce, "gzip", filePath)
+	if string(ce) != "br" {
+		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "br", filePath)
+	}
+
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
 	}
-	zbody, err := resp.BodyGunzip()
+
+	body, err := resp.BodyUnbrotli()
 	if err != nil {
-		t.Errorf("unexpected error when gunzipping response body: %v. filePath=%q", err, filePath)
+		t.Fatalf("unexpected error on unbrotli response body: %v. filePath=%q", err, filePath)
 	}
-	if string(zbody) != body {
-		t.Errorf("unexpected body len=%d. Expected len=%d. FilePath=%q", len(zbody), len(body), filePath)
+	if !bytes.Equal(body, expectedBody) {
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
 	}
 
-	// request compressed brotli file
+	// should prefer zstd over gzip and ignore unknown encoding
 	ctx.Request.Reset()
 	ctx.Request.SetRequestURI(filePath)
-	ctx.Request.Header.Set(HeaderAcceptEncoding, "br")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
 	h(&ctx)
+
 	s = ctx.Response.String()
 	br = bufio.NewReader(bytes.NewBufferString(s))
 	if err = resp.Read(br); err != nil {
-		t.Errorf("unexpected error: %v. filePath=%q", err, filePath)
+		t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
 	}
 	if resp.StatusCode() != StatusOK {
-		t.Errorf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
+		t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
 	}
+
 	ce = resp.Header.ContentEncoding()
-	if string(ce) != "br" {
-		t.Errorf("unexpected content-encoding %q. Expecting %q. filePath=%q", ce, "br", filePath)
+	if string(ce) != "zstd" {
+		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "zstd", filePath)
+	}
+
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
+	}
+
+	body, err = resp.BodyUnzstd()
+	if err != nil {
+		t.Fatalf("unexpected error on unzstd response body: %v. filePath=%q", err, filePath)
+	}
+	if !bytes.Equal(body, expectedBody) {
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
+	}
+
+	// should prefer gzip and ignore unknown encoding
+	ctx.Request.Reset()
+	ctx.Request.SetRequestURI(filePath)
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
+	h(&ctx)
+
+	s = ctx.Response.String()
+	br = bufio.NewReader(bytes.NewBufferString(s))
+	if err = resp.Read(br); err != nil {
+		t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
+	}
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
+	}
+
+	ce = resp.Header.ContentEncoding()
+	if string(ce) != "gzip" {
+		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "gzip", filePath)
 	}
-	zbody, err = resp.BodyUnbrotli()
+
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
+	}
+
+	body, err = resp.BodyGunzip()
 	if err != nil {
-		t.Errorf("unexpected error when unbrotling response body: %v. filePath=%q", err, filePath)
+		t.Fatalf("unexpected error on gunzip response body: %v. filePath=%q", err, filePath)
 	}
-	if string(zbody) != body {
-		t.Errorf("unexpected body len=%d. Expected len=%d. FilePath=%q", len(zbody), len(body), filePath)
+	if !bytes.Equal(body, expectedBody) {
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
 	}
 }
 
@@ -413,61 +559,107 @@ func TestDirFSServeFileCompressed(t *testing.T) {
 
 	var resp Response
 
-	// request compressed gzip file
+	expectedBody, err := getFileContents("/fs.go")
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	// should prefer brotli over zstd, gzip and ignore unknown encoding
 	ctx.Request.SetRequestURI("http://foobar.com/baz")
-	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
 	ServeFS(&ctx, dirTestFilesystem, "fs.go")
 
 	s := ctx.Response.String()
 	br := bufio.NewReader(bytes.NewBufferString(s))
-	if err := resp.Read(br); err != nil {
+	if err = resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
+	}
 
 	ce := resp.Header.ContentEncoding()
-	if string(ce) != "gzip" {
-		t.Fatalf("Unexpected 'Content-Encoding' %q. Expecting %q", ce, "gzip")
+	if string(ce) != "br" {
+		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "br")
 	}
 
-	body, err := resp.BodyGunzip()
-	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
+	vary := resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
 	}
-	expectedBody, err := getFileContents("/fs.go")
+
+	body, err := resp.BodyUnbrotli()
 	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
+		t.Fatalf("unexpected error on unbrotli response body: %v", err)
 	}
 	if !bytes.Equal(body, expectedBody) {
-		t.Fatalf("unexpected body %q. expecting %q", body, expectedBody)
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
 	}
 
-	// request compressed brotli file
+	// should prefer zstd over gzip and ignore unknown encoding
 	ctx.Request.Reset()
 	ctx.Request.SetRequestURI("http://foobar.com/baz")
-	ctx.Request.Header.Set(HeaderAcceptEncoding, "br")
-	ServeFS(&ctx, fsTestFilesystem, "fs.go")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
+	ServeFS(&ctx, dirTestFilesystem, "fs.go")
 
 	s = ctx.Response.String()
 	br = bufio.NewReader(bytes.NewBufferString(s))
 	if err = resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
+	}
 
 	ce = resp.Header.ContentEncoding()
-	if string(ce) != "br" {
-		t.Fatalf("Unexpected 'Content-Encoding' %q. Expecting %q", ce, "br")
+	if string(ce) != "zstd" {
+		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "zstd")
 	}
 
-	body, err = resp.BodyUnbrotli()
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
+	}
+
+	body, err = resp.BodyUnzstd()
 	if err != nil {
+		t.Fatalf("unexpected error on unzstd response body: %v", err)
+	}
+	if !bytes.Equal(body, expectedBody) {
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
+	}
+
+	// should prefer gzip and ignore unknown encoding
+	ctx.Request.Reset()
+	ctx.Request.SetRequestURI("http://foobar.com/baz")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
+	ServeFS(&ctx, dirTestFilesystem, "fs.go")
+
+	s = ctx.Response.String()
+	br = bufio.NewReader(bytes.NewBufferString(s))
+	if err = resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
-	expectedBody, err = getFileContents("/fs.go")
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
+	}
+
+	ce = resp.Header.ContentEncoding()
+	if string(ce) != "gzip" {
+		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "gzip")
+	}
+
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
+	}
+
+	body, err = resp.BodyGunzip()
 	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
+		t.Fatalf("unexpected error on gunzip response body: %v", err)
 	}
 	if !bytes.Equal(body, expectedBody) {
-		t.Fatalf("unexpected body %q. expecting %q", body, expectedBody)
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
 	}
 }
 
@@ -536,6 +728,7 @@ func TestDirFSFSCompressConcurrent(t *testing.T) {
 		GenerateIndexPages: true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CompressZstd:       true,
 		CleanStop:          stop,
 	}
 	h := fs.NewRequestHandler()
@@ -574,6 +767,7 @@ func TestDirFSFSCompressSingleThread(t *testing.T) {
 		GenerateIndexPages: true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CompressZstd:       true,
 		CleanStop:          stop,
 	}
 	h := fs.NewRequestHandler()
diff --git a/fs_test.go b/fs_test.go
index 1f7df00..b68ef6a 100644
--- a/fs_test.go
+++ b/fs_test.go
@@ -208,38 +208,47 @@ func TestServeFileCompressed(t *testing.T) {
 
 	var resp Response
 
-	// request compressed gzip file
+	expectedBody, err := getFileContents("/fs.go")
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	// should prefer brotli over zstd, gzip and ignore unknown encoding
 	ctx.Request.SetRequestURI("http://foobar.com/baz")
-	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
 	ServeFile(&ctx, "fs.go")
 
 	s := ctx.Response.String()
 	br := bufio.NewReader(bytes.NewBufferString(s))
-	if err := resp.Read(br); err != nil {
+	if err = resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
+	}
 
 	ce := resp.Header.ContentEncoding()
-	if string(ce) != "gzip" {
-		t.Fatalf("Unexpected 'Content-Encoding' %q. Expecting %q", ce, "gzip")
+	if string(ce) != "br" {
+		t.Fatalf("unexpected 'Content-Encoding': %q. Expecting %q", string(ce), "br")
 	}
 
-	body, err := resp.BodyGunzip()
-	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
+	vary := resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
 	}
-	expectedBody, err := getFileContents("/fs.go")
+
+	body, err := resp.BodyUnbrotli()
 	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
+		t.Fatalf("unexpected error on unbrotli response body: %v", err)
 	}
 	if !bytes.Equal(body, expectedBody) {
-		t.Fatalf("unexpected body %q. expecting %q", body, expectedBody)
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
 	}
 
-	// request compressed brotli file
+	// should prefer zstd over gzip and ignore unknown encoding
 	ctx.Request.Reset()
 	ctx.Request.SetRequestURI("http://foobar.com/baz")
-	ctx.Request.Header.Set(HeaderAcceptEncoding, "br")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
 	ServeFile(&ctx, "fs.go")
 
 	s = ctx.Response.String()
@@ -247,22 +256,59 @@ func TestServeFileCompressed(t *testing.T) {
 	if err = resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
+	}
 
 	ce = resp.Header.ContentEncoding()
-	if string(ce) != "br" {
-		t.Fatalf("Unexpected 'Content-Encoding' %q. Expecting %q", ce, "br")
+	if string(ce) != "zstd" {
+		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "zstd")
 	}
 
-	body, err = resp.BodyUnbrotli()
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
+	}
+
+	body, err = resp.BodyUnzstd()
 	if err != nil {
+		t.Fatalf("unexpected error on unzstd response body: %v", err)
+	}
+	if !bytes.Equal(body, expectedBody) {
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
+	}
+
+	// should prefer gzip and ignore unknown encoding
+	ctx.Request.Reset()
+	ctx.Request.SetRequestURI("http://foobar.com/baz")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
+	ServeFile(&ctx, "fs.go")
+
+	s = ctx.Response.String()
+	br = bufio.NewReader(bytes.NewBufferString(s))
+	if err = resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
-	expectedBody, err = getFileContents("/fs.go")
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
+	}
+
+	ce = resp.Header.ContentEncoding()
+	if string(ce) != "gzip" {
+		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "gzip")
+	}
+
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
+	}
+
+	body, err = resp.BodyGunzip()
 	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
+		t.Fatalf("unexpected error on gunzip response body: %v", err)
 	}
 	if !bytes.Equal(body, expectedBody) {
-		t.Fatalf("unexpected body %q. expecting %q", body, expectedBody)
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
 	}
 }
 
@@ -270,32 +316,41 @@ func TestServeFileUncompressed(t *testing.T) {
 	// This test can't run parallel as files in / might be changed by other tests.
 
 	var ctx RequestCtx
-	var req Request
-	req.SetRequestURI("http://foobar.com/baz")
-	req.Header.Set(HeaderAcceptEncoding, "gzip")
-	ctx.Init(&req, nil, nil)
+	ctx.Init(&Request{}, nil, nil)
+
+	var resp Response
+
+	expectedBody, err := getFileContents("/fs.go")
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
 
+	ctx.Request.SetRequestURI("http://foobar.com/baz")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
 	ServeFileUncompressed(&ctx, "fs.go")
 
-	var resp Response
 	s := ctx.Response.String()
 	br := bufio.NewReader(bytes.NewBufferString(s))
-	if err := resp.Read(br); err != nil {
+	if err = resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
+	}
 
 	ce := resp.Header.ContentEncoding()
 	if len(ce) > 0 {
-		t.Fatalf("Unexpected 'Content-Encoding' %q", ce)
+		t.Fatalf("unexpected 'Content-Encoding': %q. Expecting \"\"", string(ce))
 	}
 
-	body := resp.Body()
-	expectedBody, err := getFileContents("/fs.go")
-	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
+	vary := resp.Header.PeekBytes(strVary)
+	if len(vary) > 0 {
+		t.Fatalf("unexpected 'Vary': %q. Expecting \"\"", string(vary))
 	}
+
+	body := resp.Body()
 	if !bytes.Equal(body, expectedBody) {
-		t.Fatalf("unexpected body %q. expecting %q", body, expectedBody)
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
 	}
 }
 
@@ -535,6 +590,7 @@ func TestFSCompressConcurrent(t *testing.T) {
 		GenerateIndexPages: true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CompressZstd:       true,
 		CleanStop:          stop,
 	})
 }
@@ -555,6 +611,7 @@ func TestFSCompressConcurrentSkipCache(t *testing.T) {
 		SkipCache:          true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CompressZstd:       true,
 		CleanStop:          stop,
 	})
 }
@@ -597,6 +654,7 @@ func TestFSCompressSingleThread(t *testing.T) {
 		GenerateIndexPages: true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CompressZstd:       true,
 		CleanStop:          stop,
 	})
 }
@@ -613,6 +671,7 @@ func TestFSCompressSingleThreadSkipCache(t *testing.T) {
 		SkipCache:          true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CompressZstd:       true,
 		CleanStop:          stop,
 	})
 }
@@ -640,72 +699,128 @@ func testFSCompress(t *testing.T, h RequestHandler, filePath string) {
 
 	var resp Response
 
-	// request uncompressed file
-	ctx.Request.Reset()
+	// get uncompressed
 	ctx.Request.SetRequestURI(filePath)
 	h(&ctx)
+
 	s := ctx.Response.String()
 	br := bufio.NewReader(bytes.NewBufferString(s))
 	if err := resp.Read(br); err != nil {
-		t.Errorf("unexpected error: %v. filePath=%q", err, filePath)
+		t.Fatalf("unexpected error: %v", err)
 	}
 	if resp.StatusCode() != StatusOK {
-		t.Errorf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
+		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
 	}
+
 	ce := resp.Header.ContentEncoding()
-	if len(ce) != 0 {
-		t.Errorf("unexpected content-encoding %q. Expecting empty string. filePath=%q", ce, filePath)
+	if len(ce) > 0 {
+		t.Fatalf("unexpected 'Content-Encoding': %q. Expecting \"\"", string(ce))
 	}
-	body := string(resp.Body())
 
-	// request compressed gzip file
+	vary := resp.Header.PeekBytes(strVary)
+	if len(vary) > 0 {
+		t.Fatalf("unexpected 'Vary': %q. Expecting \"\"", string(vary))
+	}
+
+	expectedBody := bytes.Clone(resp.Body())
+
+	// should prefer brotli over zstd, gzip and ignore unknown encoding
 	ctx.Request.Reset()
 	ctx.Request.SetRequestURI(filePath)
-	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
 	h(&ctx)
+
 	s = ctx.Response.String()
 	br = bufio.NewReader(bytes.NewBufferString(s))
 	if err := resp.Read(br); err != nil {
-		t.Errorf("unexpected error: %v. filePath=%q", err, filePath)
+		t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
 	}
 	if resp.StatusCode() != StatusOK {
-		t.Errorf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
+		t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
 	}
+
 	ce = resp.Header.ContentEncoding()
-	if string(ce) != "gzip" {
-		t.Errorf("unexpected content-encoding %q. Expecting %q. filePath=%q", ce, "gzip", filePath)
+	if string(ce) != "br" {
+		t.Fatalf("unexpected 'Content-Encoding': %q. Expecting %q. filePath=%q", string(ce), "br", filePath)
 	}
-	zbody, err := resp.BodyGunzip()
+
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
+	}
+
+	body, err := resp.BodyUnbrotli()
 	if err != nil {
-		t.Errorf("unexpected error when gunzipping response body: %v. filePath=%q", err, filePath)
+		t.Fatalf("unexpected error on unbrotli response body: %v. filePath=%q", err, filePath)
 	}
-	if string(zbody) != body {
-		t.Errorf("unexpected body len=%d. Expected len=%d. FilePath=%q", len(zbody), len(body), filePath)
+	if !bytes.Equal(body, expectedBody) {
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
 	}
 
-	// request compressed brotli file
+	// should prefer zstd over gzip and ignore unknown encoding
 	ctx.Request.Reset()
 	ctx.Request.SetRequestURI(filePath)
-	ctx.Request.Header.Set(HeaderAcceptEncoding, "br")
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
 	h(&ctx)
+
 	s = ctx.Response.String()
 	br = bufio.NewReader(bytes.NewBufferString(s))
 	if err = resp.Read(br); err != nil {
-		t.Errorf("unexpected error: %v. filePath=%q", err, filePath)
+		t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
 	}
 	if resp.StatusCode() != StatusOK {
-		t.Errorf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
+		t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
 	}
+
 	ce = resp.Header.ContentEncoding()
-	if string(ce) != "br" {
-		t.Errorf("unexpected content-encoding %q. Expecting %q. filePath=%q", ce, "br", filePath)
+	if string(ce) != "zstd" {
+		t.Fatalf("unexpected 'Content-Encoding': %q. Expecting %q. filePath=%q", string(ce), "zstd", filePath)
+	}
+
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
+	}
+
+	body, err = resp.BodyUnzstd()
+	if err != nil {
+		t.Fatalf("unexpected error on unzstd response body: %v. filePath=%q", err, filePath)
+	}
+	if !bytes.Equal(body, expectedBody) {
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
+	}
+
+	// should prefer gzip and ignore unknown encoding
+	ctx.Request.Reset()
+	ctx.Request.SetRequestURI(filePath)
+	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
+	h(&ctx)
+
+	s = ctx.Response.String()
+	br = bufio.NewReader(bytes.NewBufferString(s))
+	if err = resp.Read(br); err != nil {
+		t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
+	}
+	if resp.StatusCode() != StatusOK {
+		t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
+	}
+
+	ce = resp.Header.ContentEncoding()
+	if string(ce) != "gzip" {
+		t.Fatalf("unexpected 'Content-Encoding': %q. Expecting %q. filePath=%q", string(ce), "gzip", filePath)
 	}
-	zbody, err = resp.BodyUnbrotli()
+
+	vary = resp.Header.PeekBytes(strVary)
+	if !bytes.Equal(vary, strAcceptEncoding) {
+		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
+	}
+
+	body, err = resp.BodyGunzip()
 	if err != nil {
-		t.Errorf("unexpected error when unbrotling response body: %v. filePath=%q", err, filePath)
+		t.Fatalf("unexpected error on gunzip response body: %v. filePath=%q", err, filePath)
 	}
-	if string(zbody) != body {
-		t.Errorf("unexpected body len=%d. Expected len=%d. FilePath=%q", len(zbody), len(body), filePath)
+	if !bytes.Equal(body, expectedBody) {
+		t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
 	}
 }
 
@@ -956,7 +1071,7 @@ func TestFileCacheForZstd(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	fs := FS{Root: os.TempDir(), Compress: true, CacheDuration: time.Second * 60}
+	fs := FS{Root: os.TempDir(), Compress: true, CompressZstd: true, CacheDuration: time.Second * 60}
 	h := fs.NewRequestHandler()
 	var ctx RequestCtx
 	var req Request