Skip to content

Commit b9936a3

Browse files
gabyReneWerner87
andauthored
🔥 Feature: Add support for zstd compression (#3041)
* Add support for zstd compression * Update whats_new.md * Add benchmarks for Compress middleware --------- Co-authored-by: RW <[email protected]>
1 parent dd26256 commit b9936a3

File tree

15 files changed

+526
-103
lines changed

15 files changed

+526
-103
lines changed

.github/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ Here is a list of middleware that are included within the Fiber framework.
581581
| [adaptor](https://github.com/gofiber/fiber/tree/main/middleware/adaptor) | Converter for net/http handlers to/from Fiber request handlers. |
582582
| [basicauth](https://github.com/gofiber/fiber/tree/main/middleware/basicauth) | Provides HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. |
583583
| [cache](https://github.com/gofiber/fiber/tree/main/middleware/cache) | Intercept and cache HTTP responses. |
584-
| [compress](https://github.com/gofiber/fiber/tree/main/middleware/compress) | Compression middleware for Fiber, with support for `deflate`, `gzip` and `brotli`. |
584+
| [compress](https://github.com/gofiber/fiber/tree/main/middleware/compress) | Compression middleware for Fiber, with support for `deflate`, `gzip`, `brotli` and `zstd`. |
585585
| [cors](https://github.com/gofiber/fiber/tree/main/middleware/cors) | Enable cross-origin resource sharing (CORS) with various options. |
586586
| [csrf](https://github.com/gofiber/fiber/tree/main/middleware/csrf) | Protect from CSRF exploits. |
587587
| [earlydata](https://github.com/gofiber/fiber/tree/main/middleware/earlydata) | Adds support for TLS 1.3's early data ("0-RTT") feature. |

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@
1919

2020
# Misc
2121
*.fiber.gz
22+
*.fiber.zst
23+
*.fiber.br
2224
*.fasthttp.gz
25+
*.fasthttp.zst
26+
*.fasthttp.br
27+
*.test.gz
28+
*.test.zst
29+
*.test.br
2330
*.pprof
2431
*.workspace
2532

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ lint:
3737
test:
3838
go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -shuffle=on
3939

40+
## longtest: 🚦 Execute all tests 10x
41+
.PHONY: longtest
42+
longtest:
43+
go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=10 -shuffle=on
44+
4045
## tidy: 📌 Clean and tidy dependencies
4146
.PHONY: tidy
4247
tidy:

app.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,11 @@ type Config struct {
219219
// Default: 4096
220220
WriteBufferSize int `json:"write_buffer_size"`
221221

222-
// CompressedFileSuffix adds suffix to the original file name and
222+
// CompressedFileSuffixes adds suffix to the original file name and
223223
// tries saving the resulting compressed file under the new file name.
224224
//
225-
// Default: ".fiber.gz"
226-
CompressedFileSuffix string `json:"compressed_file_suffix"`
225+
// Default: map[string]string{"gzip": ".fiber.gz", "br": ".fiber.br", "zstd": ".fiber.zst"}
226+
CompressedFileSuffixes map[string]string `json:"compressed_file_suffixes"`
227227

228228
// ProxyHeader will enable c.IP() to return the value of the given header key
229229
// By default c.IP() will return the Remote IP from the TCP connection
@@ -391,11 +391,10 @@ type RouteMessage struct {
391391

392392
// Default Config values
393393
const (
394-
DefaultBodyLimit = 4 * 1024 * 1024
395-
DefaultConcurrency = 256 * 1024
396-
DefaultReadBufferSize = 4096
397-
DefaultWriteBufferSize = 4096
398-
DefaultCompressedFileSuffix = ".fiber.gz"
394+
DefaultBodyLimit = 4 * 1024 * 1024
395+
DefaultConcurrency = 256 * 1024
396+
DefaultReadBufferSize = 4096
397+
DefaultWriteBufferSize = 4096
399398
)
400399

401400
// HTTP methods enabled by default
@@ -477,9 +476,14 @@ func New(config ...Config) *App {
477476
if app.config.WriteBufferSize <= 0 {
478477
app.config.WriteBufferSize = DefaultWriteBufferSize
479478
}
480-
if app.config.CompressedFileSuffix == "" {
481-
app.config.CompressedFileSuffix = DefaultCompressedFileSuffix
479+
if app.config.CompressedFileSuffixes == nil {
480+
app.config.CompressedFileSuffixes = map[string]string{
481+
"gzip": ".fiber.gz",
482+
"br": ".fiber.br",
483+
"zstd": ".fiber.zst",
484+
}
482485
}
486+
483487
if app.config.Immutable {
484488
app.getBytes, app.getString = getBytesImmutable, getStringImmutable
485489
}

bind_test.go

Lines changed: 90 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -824,35 +824,61 @@ func Benchmark_Bind_RespHeader_Map(b *testing.B) {
824824
require.NoError(b, err)
825825
}
826826

827-
// go test -run Test_Bind_Body
827+
// go test -run Test_Bind_Body_Compression
828828
func Test_Bind_Body(t *testing.T) {
829829
t.Parallel()
830830
app := New()
831-
c := app.AcquireCtx(&fasthttp.RequestCtx{})
831+
reqBody := []byte(`{"name":"john"}`)
832832

833833
type Demo struct {
834834
Name string `json:"name" xml:"name" form:"name" query:"name"`
835835
}
836836

837-
{
838-
var gzipJSON bytes.Buffer
839-
w := gzip.NewWriter(&gzipJSON)
840-
_, err := w.Write([]byte(`{"name":"john"}`))
841-
require.NoError(t, err)
842-
err = w.Close()
843-
require.NoError(t, err)
844-
837+
// Helper function to test compressed bodies
838+
testCompressedBody := func(t *testing.T, compressedBody []byte, encoding string) {
839+
t.Helper()
840+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
845841
c.Request().Header.SetContentType(MIMEApplicationJSON)
846-
c.Request().Header.Set(HeaderContentEncoding, "gzip")
847-
c.Request().SetBody(gzipJSON.Bytes())
848-
c.Request().Header.SetContentLength(len(gzipJSON.Bytes()))
842+
c.Request().Header.Set(fasthttp.HeaderContentEncoding, encoding)
843+
c.Request().SetBody(compressedBody)
844+
c.Request().Header.SetContentLength(len(compressedBody))
849845
d := new(Demo)
850846
require.NoError(t, c.Bind().Body(d))
851847
require.Equal(t, "john", d.Name)
852-
c.Request().Header.Del(HeaderContentEncoding)
848+
c.Request().Header.Del(fasthttp.HeaderContentEncoding)
853849
}
854850

855-
testDecodeParser := func(contentType, body string) {
851+
t.Run("Gzip", func(t *testing.T) {
852+
t.Parallel()
853+
compressedBody := fasthttp.AppendGzipBytes(nil, reqBody)
854+
require.NotEqual(t, reqBody, compressedBody)
855+
testCompressedBody(t, compressedBody, "gzip")
856+
})
857+
858+
t.Run("Deflate", func(t *testing.T) {
859+
t.Parallel()
860+
compressedBody := fasthttp.AppendDeflateBytes(nil, reqBody)
861+
require.NotEqual(t, reqBody, compressedBody)
862+
testCompressedBody(t, compressedBody, "deflate")
863+
})
864+
865+
t.Run("Brotli", func(t *testing.T) {
866+
t.Parallel()
867+
compressedBody := fasthttp.AppendBrotliBytes(nil, reqBody)
868+
require.NotEqual(t, reqBody, compressedBody)
869+
testCompressedBody(t, compressedBody, "br")
870+
})
871+
872+
t.Run("Zstd", func(t *testing.T) {
873+
t.Parallel()
874+
compressedBody := fasthttp.AppendZstdBytes(nil, reqBody)
875+
require.NotEqual(t, reqBody, compressedBody)
876+
testCompressedBody(t, compressedBody, "zstd")
877+
})
878+
879+
testDecodeParser := func(t *testing.T, contentType, body string) {
880+
t.Helper()
881+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
856882
c.Request().Header.SetContentType(contentType)
857883
c.Request().SetBody([]byte(body))
858884
c.Request().Header.SetContentLength(len(body))
@@ -861,44 +887,68 @@ func Test_Bind_Body(t *testing.T) {
861887
require.Equal(t, "john", d.Name)
862888
}
863889

864-
testDecodeParser(MIMEApplicationJSON, `{"name":"john"}`)
865-
testDecodeParser(MIMEApplicationXML, `<Demo><name>john</name></Demo>`)
866-
testDecodeParser(MIMEApplicationForm, "name=john")
867-
testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--")
890+
t.Run("JSON", func(t *testing.T) {
891+
testDecodeParser(t, MIMEApplicationJSON, `{"name":"john"}`)
892+
})
893+
894+
t.Run("XML", func(t *testing.T) {
895+
testDecodeParser(t, MIMEApplicationXML, `<Demo><name>john</name></Demo>`)
896+
})
897+
898+
t.Run("Form", func(t *testing.T) {
899+
testDecodeParser(t, MIMEApplicationForm, "name=john")
900+
})
868901

869-
testDecodeParserError := func(contentType, body string) {
902+
t.Run("MultipartForm", func(t *testing.T) {
903+
testDecodeParser(t, MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--")
904+
})
905+
906+
testDecodeParserError := func(t *testing.T, contentType, body string) {
907+
t.Helper()
908+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
870909
c.Request().Header.SetContentType(contentType)
871910
c.Request().SetBody([]byte(body))
872911
c.Request().Header.SetContentLength(len(body))
873912
require.Error(t, c.Bind().Body(nil))
874913
}
875914

876-
testDecodeParserError("invalid-content-type", "")
877-
testDecodeParserError(MIMEMultipartForm+`;boundary="b"`, "--b")
915+
t.Run("ErrorInvalidContentType", func(t *testing.T) {
916+
testDecodeParserError(t, "invalid-content-type", "")
917+
})
918+
919+
t.Run("ErrorMalformedMultipart", func(t *testing.T) {
920+
testDecodeParserError(t, MIMEMultipartForm+`;boundary="b"`, "--b")
921+
})
878922

879923
type CollectionQuery struct {
880924
Data []Demo `query:"data"`
881925
}
882926

883-
c.Request().Reset()
884-
c.Request().Header.SetContentType(MIMEApplicationForm)
885-
c.Request().SetBody([]byte("data[0][name]=john&data[1][name]=doe"))
886-
c.Request().Header.SetContentLength(len(c.Body()))
887-
cq := new(CollectionQuery)
888-
require.NoError(t, c.Bind().Body(cq))
889-
require.Len(t, cq.Data, 2)
890-
require.Equal(t, "john", cq.Data[0].Name)
891-
require.Equal(t, "doe", cq.Data[1].Name)
927+
t.Run("CollectionQuerySquareBrackets", func(t *testing.T) {
928+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
929+
c.Request().Reset()
930+
c.Request().Header.SetContentType(MIMEApplicationForm)
931+
c.Request().SetBody([]byte("data[0][name]=john&data[1][name]=doe"))
932+
c.Request().Header.SetContentLength(len(c.Body()))
933+
cq := new(CollectionQuery)
934+
require.NoError(t, c.Bind().Body(cq))
935+
require.Len(t, cq.Data, 2)
936+
require.Equal(t, "john", cq.Data[0].Name)
937+
require.Equal(t, "doe", cq.Data[1].Name)
938+
})
892939

893-
c.Request().Reset()
894-
c.Request().Header.SetContentType(MIMEApplicationForm)
895-
c.Request().SetBody([]byte("data.0.name=john&data.1.name=doe"))
896-
c.Request().Header.SetContentLength(len(c.Body()))
897-
cq = new(CollectionQuery)
898-
require.NoError(t, c.Bind().Body(cq))
899-
require.Len(t, cq.Data, 2)
900-
require.Equal(t, "john", cq.Data[0].Name)
901-
require.Equal(t, "doe", cq.Data[1].Name)
940+
t.Run("CollectionQueryDotNotation", func(t *testing.T) {
941+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
942+
c.Request().Reset()
943+
c.Request().Header.SetContentType(MIMEApplicationForm)
944+
c.Request().SetBody([]byte("data.0.name=john&data.1.name=doe"))
945+
c.Request().Header.SetContentLength(len(c.Body()))
946+
cq := new(CollectionQuery)
947+
require.NoError(t, c.Bind().Body(cq))
948+
require.Len(t, cq.Data, 2)
949+
require.Equal(t, "john", cq.Data[0].Name)
950+
require.Equal(t, "doe", cq.Data[1].Name)
951+
})
902952
}
903953

904954
// go test -run Test_Bind_Body_WithSetParserDecoder

constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ const (
300300
StrBr = "br"
301301
StrDeflate = "deflate"
302302
StrBrotli = "brotli"
303+
StrZstd = "zstd"
303304
)
304305

305306
// Cookie SameSite

ctx.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ func (c *DefaultCtx) tryDecodeBodyInOrder(
218218
body, err = c.fasthttp.Request.BodyUnbrotli()
219219
case StrDeflate:
220220
body, err = c.fasthttp.Request.BodyInflate()
221+
case StrZstd:
222+
body, err = c.fasthttp.Request.BodyUnzstd()
221223
default:
222224
decodesRealized--
223225
if len(encodings) == 1 {
@@ -1429,14 +1431,15 @@ func (c *DefaultCtx) SendFile(file string, compress ...bool) error {
14291431
sendFileOnce.Do(func() {
14301432
const cacheDuration = 10 * time.Second
14311433
sendFileFS = &fasthttp.FS{
1432-
Root: "",
1433-
AllowEmptyRoot: true,
1434-
GenerateIndexPages: false,
1435-
AcceptByteRange: true,
1436-
Compress: true,
1437-
CompressedFileSuffix: c.app.config.CompressedFileSuffix,
1438-
CacheDuration: cacheDuration,
1439-
IndexNames: []string{"index.html"},
1434+
Root: "",
1435+
AllowEmptyRoot: true,
1436+
GenerateIndexPages: false,
1437+
AcceptByteRange: true,
1438+
Compress: true,
1439+
CompressBrotli: true,
1440+
CompressedFileSuffixes: c.app.config.CompressedFileSuffixes,
1441+
CacheDuration: cacheDuration,
1442+
IndexNames: []string{"index.html"},
14401443
PathNotFound: func(ctx *fasthttp.RequestCtx) {
14411444
ctx.Response.SetStatusCode(StatusNotFound)
14421445
},

0 commit comments

Comments
 (0)