Repository: valyala/fasthttp
Pull request #1945 (2025-01-28 06:48:29)

perf: split delAllArgs into delAllArgs and delAllArgsStable
The original `delAllArgs` method is stable but requires more copying operations to maintain the order of elements.

- Renamed the original `delAllArgs` method to `delAllArgsStable` to maintain stable behavior.
- Added a new `delAllArgs` method for non-stable functionality, improving runtime efficiency.

Depending on the specific use case:

+ Use `delAllArgsStable` when stability (order preservation) is required.
+ Use `delAllArgs` when stability is not necessary.

In the case of handling headers in `h.h`, the order of headers is not important. Refer to net/http [header.go](https://cs.opensource.google/go/go/+/refs/tags/go1.23.5:src/net/http/header.go;l=80-82;drc=4f0408a3a205a88624dced4b188e11dd429bc3ad), where the Del function calls [textproto.MIMEHeader.Del](https://cs.opensource.google/go/go/+/refs/tags/go1.23.5:src/net/textproto/header.go;l=54-56;drc=1d45a7ef560a76318ed59dfdb178cecd58caf948). This function is implemented using a map, and the map delete operation is inherently unstable.

Benchmark compare `delAllArgsStable` and `delAllArgs`

```go
func BenchmarkArgsDel(b *testing.B) {
	for i := 0; i < b.N; i++ {
		b.StopTimer()
		var a []argsKV
		for j := 1; j < 10; j++ {
			a = appendArg(a, fmt.Sprintf("item%d", j), "val", argsHasValue)
		}
		for j := 1; j < 10; j++ {
			a = appendArg(a, fmt.Sprintf("item%d", j), "val", argsHasValue)
		}
		b.StartTimer()
		for j := 1; j < 10; j++ {
			a = delAllArgs(a, fmt.Sprintf("item%d", j))
		}
	}
}

func BenchmarkArgsDelStable(b *testing.B) {
	for i := 0; i < b.N; i++ {
		b.StopTimer()
		var a []argsKV
		for j := 1; j < 10; j++ {
			a = appendArg(a, fmt.Sprintf("item%d", j), "val", argsHasValue)
		}
		for j := 1; j < 10; j++ {
			a = appendArg(a, fmt.Sprintf("item%d", j), "val", argsHasValue)
		}
		b.StartTimer()
		for j := 1; j < 10; j++ {
			a = delAllArgsStable(a, fmt.Sprintf("item%d", j))
		}
	}
}
```

Benchmark result:

```
$ go test --run=^$ --bench BenchmarkArgsDel -benchtime=100000x
goos: linux
goarch: amd64
pkg: github.com/valyala/fasthttp
cpu: AMD EPYC 7763 64-Core Processor                
BenchmarkArgsDel-4                100000              2072 ns/op
BenchmarkArgsDelStable-4          100000              2370 ns/op
PASS
ok      github.com/valyala/fasthttp     8.312s
```


Patch:
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index bea625c..f04b5ee 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -16,6 +16,6 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Run Gosec Security Scanner
-        uses: securego/gosec@v2.22.0
+        uses: securego/gosec@v2.22.1
         with:
           args: '-exclude=G103,G104,G304,G402 ./...'
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/args.go b/args.go
index dde4c0b..254c5d5 100644
--- a/args.go
+++ b/args.go
@@ -155,12 +155,12 @@ func (a *Args) WriteTo(w io.Writer) (int64, error) {
 
 // Del deletes argument with the given key from query args.
 func (a *Args) Del(key string) {
-	a.args = delAllArgs(a.args, key)
+	a.args = delAllArgsStable(a.args, key)
 }
 
 // DelBytes deletes argument with the given key from query args.
 func (a *Args) DelBytes(key []byte) {
-	a.args = delAllArgs(a.args, b2s(key))
+	a.args = delAllArgsStable(a.args, b2s(key))
 }
 
 // Add adds 'key=value' argument.
@@ -356,13 +356,6 @@ func visitArgs(args []argsKV, f func(k, v []byte)) {
 	}
 }
 
-func visitArgsKey(args []argsKV, f func(k []byte)) {
-	for i, n := 0, len(args); i < n; i++ {
-		kv := &args[i]
-		f(kv.key)
-	}
-}
-
 func copyArgs(dst, src []argsKV) []argsKV {
 	if cap(dst) < len(src) {
 		tmp := make([]argsKV, len(src))
@@ -392,11 +385,7 @@ func copyArgs(dst, src []argsKV) []argsKV {
 	return dst
 }
 
-func delAllArgsBytes(args []argsKV, key []byte) []argsKV {
-	return delAllArgs(args, b2s(key))
-}
-
-func delAllArgs(args []argsKV, key string) []argsKV {
+func delAllArgsStable(args []argsKV, key string) []argsKV {
 	for i, n := 0, len(args); i < n; i++ {
 		kv := &args[i]
 		if key == string(kv.key) {
@@ -411,6 +400,18 @@ func delAllArgs(args []argsKV, key string) []argsKV {
 	return args
 }
 
+func delAllArgs(args []argsKV, key string) []argsKV {
+	n := len(args)
+	for i := 0; i < n; i++ {
+		if key == string(args[i].key) {
+			args[i], args[n-1] = args[n-1], args[i]
+			n--
+			i--
+		}
+	}
+	return args[:n]
+}
+
 func setArgBytes(h []argsKV, key, value []byte, noValue bool) []argsKV {
 	return setArg(h, b2s(key), b2s(value), noValue)
 }
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 f917f30..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/sys v0.29.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 cd66b84..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/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
-golang.org/x/sys v0.29.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/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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
diff --git a/header.go b/header.go
index c2fb794..ce2b832 100644
--- a/header.go
+++ b/header.go
@@ -34,9 +34,9 @@ type ResponseHeader struct {
 	contentEncoding []byte
 	server          []byte
 	mulHeader       [][]byte
+	trailer         [][]byte
 
-	h       []argsKV
-	trailer []argsKV
+	h []argsKV
 
 	cookies []argsKV
 	bufK    []byte
@@ -72,9 +72,9 @@ type RequestHeader struct {
 	contentType []byte
 	userAgent   []byte
 	mulHeader   [][]byte
+	trailer     [][]byte
 
-	h       []argsKV
-	trailer []argsKV
+	h []argsKV
 
 	cookies []argsKV
 
@@ -193,7 +193,7 @@ func (h *ResponseHeader) SetConnectionClose() {
 func (h *ResponseHeader) ResetConnectionClose() {
 	if h.connectionClose {
 		h.connectionClose = false
-		h.h = delAllArgsBytes(h.h, strConnection)
+		h.h = delAllArgs(h.h, HeaderConnection)
 	}
 }
 
@@ -211,7 +211,7 @@ func (h *RequestHeader) SetConnectionClose() {
 func (h *RequestHeader) ResetConnectionClose() {
 	if h.connectionClose {
 		h.connectionClose = false
-		h.h = delAllArgsBytes(h.h, strConnection)
+		h.h = delAllArgs(h.h, HeaderConnection)
 	}
 }
 
@@ -251,7 +251,7 @@ func (h *ResponseHeader) SetContentLength(contentLength int) {
 	h.contentLength = contentLength
 	if contentLength >= 0 {
 		h.contentLengthBytes = AppendUint(h.contentLengthBytes[:0], contentLength)
-		h.h = delAllArgsBytes(h.h, strTransferEncoding)
+		h.h = delAllArgs(h.h, HeaderTransferEncoding)
 		return
 	} else if contentLength == -1 {
 		h.contentLengthBytes = h.contentLengthBytes[:0]
@@ -296,7 +296,7 @@ func (h *RequestHeader) SetContentLength(contentLength int) {
 	h.contentLength = contentLength
 	if contentLength >= 0 {
 		h.contentLengthBytes = AppendUint(h.contentLengthBytes[:0], contentLength)
-		h.h = delAllArgsBytes(h.h, strTransferEncoding)
+		h.h = delAllArgs(h.h, HeaderTransferEncoding)
 	} else {
 		h.contentLengthBytes = h.contentLengthBytes[:0]
 		h.h = setArgBytes(h.h, strTransferEncoding, strChunked, argsHasValue)
@@ -542,7 +542,14 @@ func (h *ResponseHeader) AddTrailerBytes(trailer []byte) error {
 		}
 		h.bufK = append(h.bufK[:0], key...)
 		normalizeHeaderKey(h.bufK, h.disableNormalizing)
-		h.trailer = appendArgBytes(h.trailer, h.bufK, nil, argsNoValue)
+		if cap(h.trailer) > len(h.trailer) {
+			h.trailer = h.trailer[:len(h.trailer)+1]
+			h.trailer[len(h.trailer)-1] = append(h.trailer[len(h.trailer)-1][:0], h.bufK...)
+		} else {
+			key = make([]byte, len(h.bufK))
+			copy(key, h.bufK)
+			h.trailer = append(h.trailer, key)
+		}
 	}
 
 	return err
@@ -890,7 +897,14 @@ func (h *RequestHeader) AddTrailerBytes(trailer []byte) error {
 		}
 		h.bufK = append(h.bufK[:0], key...)
 		normalizeHeaderKey(h.bufK, h.disableNormalizing)
-		h.trailer = appendArgBytes(h.trailer, h.bufK, nil, argsNoValue)
+		if cap(h.trailer) > len(h.trailer) {
+			h.trailer = h.trailer[:len(h.trailer)+1]
+			h.trailer[len(h.trailer)-1] = append(h.trailer[len(h.trailer)-1][:0], h.bufK...)
+		} else {
+			key = make([]byte, len(h.bufK))
+			copy(key, h.bufK)
+			h.trailer = append(h.trailer, key)
+		}
 	}
 
 	return err
@@ -1162,7 +1176,7 @@ func (h *ResponseHeader) CopyTo(dst *ResponseHeader) {
 	dst.server = append(dst.server, h.server...)
 	dst.h = copyArgs(dst.h, h.h)
 	dst.cookies = copyArgs(dst.cookies, h.cookies)
-	dst.trailer = copyArgs(dst.trailer, h.trailer)
+	dst.trailer = copyTrailer(dst.trailer, h.trailer)
 }
 
 // CopyTo copies all the headers to dst.
@@ -1182,7 +1196,7 @@ func (h *RequestHeader) CopyTo(dst *RequestHeader) {
 	dst.host = append(dst.host, h.host...)
 	dst.contentType = append(dst.contentType, h.contentType...)
 	dst.userAgent = append(dst.userAgent, h.userAgent...)
-	dst.trailer = append(dst.trailer, h.trailer...)
+	dst.trailer = copyTrailer(dst.trailer, h.trailer)
 	dst.h = copyArgs(dst.h, h.h)
 	dst.cookies = copyArgs(dst.cookies, h.cookies)
 	dst.cookiesCollected = h.cookiesCollected
@@ -1215,7 +1229,7 @@ func (h *ResponseHeader) VisitAll(f func(key, value []byte)) {
 		})
 	}
 	if len(h.trailer) > 0 {
-		f(strTrailer, appendArgsKeyBytes(nil, h.trailer, strCommaSpace))
+		f(strTrailer, appendTrailerBytes(nil, h.trailer, strCommaSpace))
 	}
 	visitArgs(h.h, f)
 	if h.ConnectionClose() {
@@ -1227,14 +1241,18 @@ func (h *ResponseHeader) VisitAll(f func(key, value []byte)) {
 //
 // f must not retain references to value after returning.
 func (h *ResponseHeader) VisitAllTrailer(f func(value []byte)) {
-	visitArgsKey(h.trailer, f)
+	for i := range h.trailer {
+		f(h.trailer[i])
+	}
 }
 
 // VisitAllTrailer calls f for each request Trailer.
 //
 // f must not retain references to value after returning.
 func (h *RequestHeader) VisitAllTrailer(f func(value []byte)) {
-	visitArgsKey(h.trailer, f)
+	for i := range h.trailer {
+		f(h.trailer[i])
+	}
 }
 
 // VisitAllCookie calls f for each response cookie.
@@ -1279,7 +1297,7 @@ func (h *RequestHeader) VisitAll(f func(key, value []byte)) {
 		f(strUserAgent, userAgent)
 	}
 	if len(h.trailer) > 0 {
-		f(strTrailer, appendArgsKeyBytes(nil, h.trailer, strCommaSpace))
+		f(strTrailer, appendTrailerBytes(nil, h.trailer, strCommaSpace))
 	}
 
 	h.collectCookies()
@@ -1342,7 +1360,7 @@ func (h *ResponseHeader) del(key []byte) {
 	case HeaderTrailer:
 		h.trailer = h.trailer[:0]
 	}
-	h.h = delAllArgsBytes(h.h, key)
+	h.h = delAllArgs(h.h, b2s(key))
 }
 
 // Del deletes header with the given key.
@@ -1376,7 +1394,7 @@ func (h *RequestHeader) del(key []byte) {
 	case HeaderTrailer:
 		h.trailer = h.trailer[:0]
 	}
-	h.h = delAllArgsBytes(h.h, key)
+	h.h = delAllArgs(h.h, b2s(key))
 }
 
 // setSpecialHeader handles special headers and return true when a header is processed.
@@ -1925,7 +1943,7 @@ func (h *ResponseHeader) peek(key []byte) []byte {
 	case HeaderSetCookie:
 		return appendResponseCookieBytes(nil, h.cookies)
 	case HeaderTrailer:
-		return appendArgsKeyBytes(nil, h.trailer, strCommaSpace)
+		return appendTrailerBytes(nil, h.trailer, strCommaSpace)
 	default:
 		return peekArgBytes(h.h, key)
 	}
@@ -1952,7 +1970,7 @@ func (h *RequestHeader) peek(key []byte) []byte {
 		}
 		return peekArgBytes(h.h, key)
 	case HeaderTrailer:
-		return appendArgsKeyBytes(nil, h.trailer, strCommaSpace)
+		return appendTrailerBytes(nil, h.trailer, strCommaSpace)
 	default:
 		return peekArgBytes(h.h, key)
 	}
@@ -1999,7 +2017,7 @@ func (h *RequestHeader) peekAll(key []byte) [][]byte {
 			h.mulHeader = peekAllArgBytesToDst(h.mulHeader, h.h, key)
 		}
 	case HeaderTrailer:
-		h.mulHeader = append(h.mulHeader, appendArgsKeyBytes(nil, h.trailer, strCommaSpace))
+		h.mulHeader = append(h.mulHeader, appendTrailerBytes(nil, h.trailer, strCommaSpace))
 	default:
 		h.mulHeader = peekAllArgBytesToDst(h.mulHeader, h.h, key)
 	}
@@ -2043,7 +2061,7 @@ func (h *ResponseHeader) peekAll(key []byte) [][]byte {
 	case HeaderSetCookie:
 		h.mulHeader = append(h.mulHeader, appendResponseCookieBytes(nil, h.cookies))
 	case HeaderTrailer:
-		h.mulHeader = append(h.mulHeader, appendArgsKeyBytes(nil, h.trailer, strCommaSpace))
+		h.mulHeader = append(h.mulHeader, appendTrailerBytes(nil, h.trailer, strCommaSpace))
 	default:
 		h.mulHeader = peekAllArgBytesToDst(h.mulHeader, h.h, key)
 	}
@@ -2069,8 +2087,7 @@ func (h *RequestHeader) PeekKeys() [][]byte {
 // Any future calls to the Peek* will modify the returned value.
 // Do not store references to returned value. Make copies instead.
 func (h *RequestHeader) PeekTrailerKeys() [][]byte {
-	h.mulHeader = h.mulHeader[:0]
-	h.mulHeader = peekArgsKeys(h.mulHeader, h.trailer)
+	h.mulHeader = copyTrailer(h.mulHeader, h.trailer)
 	return h.mulHeader
 }
 
@@ -2094,7 +2111,9 @@ func (h *ResponseHeader) PeekKeys() [][]byte {
 // Do not store references to returned value. Make copies instead.
 func (h *ResponseHeader) PeekTrailerKeys() [][]byte {
 	h.mulHeader = h.mulHeader[:0]
-	h.mulHeader = peekArgsKeys(h.mulHeader, h.trailer)
+	for i, n := 0, len(h.trailer); i < n; i++ {
+		h.mulHeader = append(h.mulHeader, h.trailer[i])
+	}
 	return h.mulHeader
 }
 
@@ -2462,8 +2481,8 @@ func (h *ResponseHeader) writeTrailer(w *bufio.Writer) error {
 func (h *ResponseHeader) TrailerHeader() []byte {
 	h.bufV = h.bufV[:0]
 	for _, t := range h.trailer {
-		value := h.peek(t.key)
-		h.bufV = appendHeaderLine(h.bufV, t.key, value)
+		value := h.peek(t)
+		h.bufV = appendHeaderLine(h.bufV, t, value)
 	}
 	h.bufV = append(h.bufV, strCRLF...)
 	return h.bufV
@@ -2523,7 +2542,7 @@ func (h *ResponseHeader) AppendBytes(dst []byte) []byte {
 		// Exclude trailer from header
 		exclude := false
 		for _, t := range h.trailer {
-			if bytes.Equal(kv.key, t.key) {
+			if bytes.Equal(kv.key, t) {
 				exclude = true
 				break
 			}
@@ -2534,7 +2553,7 @@ func (h *ResponseHeader) AppendBytes(dst []byte) []byte {
 	}
 
 	if len(h.trailer) > 0 {
-		dst = appendHeaderLine(dst, strTrailer, appendArgsKeyBytes(nil, h.trailer, strCommaSpace))
+		dst = appendHeaderLine(dst, strTrailer, appendTrailerBytes(nil, h.trailer, strCommaSpace))
 	}
 
 	n := len(h.cookies)
@@ -2594,8 +2613,8 @@ func (h *RequestHeader) writeTrailer(w *bufio.Writer) error {
 func (h *RequestHeader) TrailerHeader() []byte {
 	h.bufV = h.bufV[:0]
 	for _, t := range h.trailer {
-		value := h.peek(t.key)
-		h.bufV = appendHeaderLine(h.bufV, t.key, value)
+		value := h.peek(t)
+		h.bufV = appendHeaderLine(h.bufV, t, value)
 	}
 	h.bufV = append(h.bufV, strCRLF...)
 	return h.bufV
@@ -2656,7 +2675,7 @@ func (h *RequestHeader) AppendBytes(dst []byte) []byte {
 		// Exclude trailer from header
 		exclude := false
 		for _, t := range h.trailer {
-			if bytes.Equal(kv.key, t.key) {
+			if bytes.Equal(kv.key, t) {
 				exclude = true
 				break
 			}
@@ -2667,7 +2686,7 @@ func (h *RequestHeader) AppendBytes(dst []byte) []byte {
 	}
 
 	if len(h.trailer) > 0 {
-		dst = appendHeaderLine(dst, strTrailer, appendArgsKeyBytes(nil, h.trailer, strCommaSpace))
+		dst = appendHeaderLine(dst, strTrailer, appendTrailerBytes(nil, h.trailer, strCommaSpace))
 	}
 
 	// there is no need in h.collectCookies() here, since if cookies aren't collected yet,
@@ -3608,10 +3627,9 @@ func AppendNormalizedHeaderKeyBytes(dst, key []byte) []byte {
 	return AppendNormalizedHeaderKey(dst, b2s(key))
 }
 
-func appendArgsKeyBytes(dst []byte, args []argsKV, sep []byte) []byte {
-	for i, n := 0, len(args); i < n; i++ {
-		kv := &args[i]
-		dst = append(dst, kv.key...)
+func appendTrailerBytes(dst []byte, trailer [][]byte, sep []byte) []byte {
+	for i, n := 0, len(trailer); i < n; i++ {
+		dst = append(dst, trailer[i]...)
 		if i+1 < n {
 			dst = append(dst, sep...)
 		}
@@ -3619,6 +3637,20 @@ func appendArgsKeyBytes(dst []byte, args []argsKV, sep []byte) []byte {
 	return dst
 }
 
+func copyTrailer(dst, src [][]byte) [][]byte {
+	if cap(dst) > len(src) {
+		dst = dst[:len(src)]
+	} else {
+		dst = append(dst[:0], src...)
+	}
+
+	for i := range dst {
+		dst[i] = make([]byte, len(src[i]))
+		copy(dst[i], src[i])
+	}
+	return dst
+}
+
 var (
 	errNeedMore    = errors.New("need more data: cannot find trailing lf")
 	errInvalidName = errors.New("invalid header name")
diff --git a/tcplisten/socket.go b/tcplisten/socket.go
index 4090f12..e008f58 100644
--- a/tcplisten/socket.go
+++ b/tcplisten/socket.go
@@ -1,4 +1,4 @@
-//go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || rumprun || (zos && s390x)
+//go:build !js && !wasm && (linux || darwin || dragonfly || freebsd || netbsd || openbsd || rumprun || (zos && s390x))
 
 package tcplisten
 
diff --git a/tcplisten/socket_other.go b/tcplisten/socket_other.go
index b3289c3..65b98b0 100644
--- a/tcplisten/socket_other.go
+++ b/tcplisten/socket_other.go
@@ -1,4 +1,4 @@
-//go:build linux || dragonfly || freebsd || netbsd || openbsd || rumprun
+//go:build !js && !wasm && (linux || dragonfly || freebsd || netbsd || openbsd || rumprun)
 
 package tcplisten
 
diff --git a/tcplisten/tcplisten_js_wasm.go b/tcplisten/tcplisten_js_wasm.go
new file mode 100644
index 0000000..3c46c8e
--- /dev/null
+++ b/tcplisten/tcplisten_js_wasm.go
@@ -0,0 +1,17 @@
+package tcplisten
+
+import (
+	"net"
+)
+
+// A dummy implementation for js,wasm
+type Config struct {
+	ReusePort   bool
+	DeferAccept bool
+	FastOpen    bool
+	Backlog     int
+}
+
+func (cfg *Config) NewListener(network, addr string) (net.Listener, error) {
+	return net.Listen(network, addr)
+}


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