Skip to content

Commit

Permalink
🔥 Feature: Add support for zstd compression (#3041)
Browse files Browse the repository at this point in the history
* Add support for zstd compression

* Update whats_new.md

* Add benchmarks for Compress middleware

---------

Co-authored-by: RW <rene@gofiber.io>
  • Loading branch information
gaby and ReneWerner87 authored Jun 26, 2024
1 parent dd26256 commit b9936a3
Show file tree
Hide file tree
Showing 15 changed files with 526 additions and 103 deletions.
2 changes: 1 addition & 1 deletion .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ Here is a list of middleware that are included within the Fiber framework.
| [adaptor](https://github.com/gofiber/fiber/tree/main/middleware/adaptor) | Converter for net/http handlers to/from Fiber request handlers. |
| [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. |
| [cache](https://github.com/gofiber/fiber/tree/main/middleware/cache) | Intercept and cache HTTP responses. |
| [compress](https://github.com/gofiber/fiber/tree/main/middleware/compress) | Compression middleware for Fiber, with support for `deflate`, `gzip` and `brotli`. |
| [compress](https://github.com/gofiber/fiber/tree/main/middleware/compress) | Compression middleware for Fiber, with support for `deflate`, `gzip`, `brotli` and `zstd`. |
| [cors](https://github.com/gofiber/fiber/tree/main/middleware/cors) | Enable cross-origin resource sharing (CORS) with various options. |
| [csrf](https://github.com/gofiber/fiber/tree/main/middleware/csrf) | Protect from CSRF exploits. |
| [earlydata](https://github.com/gofiber/fiber/tree/main/middleware/earlydata) | Adds support for TLS 1.3's early data ("0-RTT") feature. |
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@

# Misc
*.fiber.gz
*.fiber.zst
*.fiber.br
*.fasthttp.gz
*.fasthttp.zst
*.fasthttp.br
*.test.gz
*.test.zst
*.test.br
*.pprof
*.workspace

Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ lint:
test:
go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -shuffle=on

## longtest: 🚦 Execute all tests 10x
.PHONY: longtest
longtest:
go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=10 -shuffle=on

## tidy: 📌 Clean and tidy dependencies
.PHONY: tidy
tidy:
Expand Down
24 changes: 14 additions & 10 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,11 @@ type Config struct {
// Default: 4096
WriteBufferSize int `json:"write_buffer_size"`

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

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

// Default Config values
const (
DefaultBodyLimit = 4 * 1024 * 1024
DefaultConcurrency = 256 * 1024
DefaultReadBufferSize = 4096
DefaultWriteBufferSize = 4096
DefaultCompressedFileSuffix = ".fiber.gz"
DefaultBodyLimit = 4 * 1024 * 1024
DefaultConcurrency = 256 * 1024
DefaultReadBufferSize = 4096
DefaultWriteBufferSize = 4096
)

// HTTP methods enabled by default
Expand Down Expand Up @@ -477,9 +476,14 @@ func New(config ...Config) *App {
if app.config.WriteBufferSize <= 0 {
app.config.WriteBufferSize = DefaultWriteBufferSize
}
if app.config.CompressedFileSuffix == "" {
app.config.CompressedFileSuffix = DefaultCompressedFileSuffix
if app.config.CompressedFileSuffixes == nil {
app.config.CompressedFileSuffixes = map[string]string{
"gzip": ".fiber.gz",
"br": ".fiber.br",
"zstd": ".fiber.zst",
}
}

if app.config.Immutable {
app.getBytes, app.getString = getBytesImmutable, getStringImmutable
}
Expand Down
130 changes: 90 additions & 40 deletions bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -824,35 +824,61 @@ func Benchmark_Bind_RespHeader_Map(b *testing.B) {
require.NoError(b, err)
}

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

type Demo struct {
Name string `json:"name" xml:"name" form:"name" query:"name"`
}

{
var gzipJSON bytes.Buffer
w := gzip.NewWriter(&gzipJSON)
_, err := w.Write([]byte(`{"name":"john"}`))
require.NoError(t, err)
err = w.Close()
require.NoError(t, err)

// Helper function to test compressed bodies
testCompressedBody := func(t *testing.T, compressedBody []byte, encoding string) {
t.Helper()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.SetContentType(MIMEApplicationJSON)
c.Request().Header.Set(HeaderContentEncoding, "gzip")
c.Request().SetBody(gzipJSON.Bytes())
c.Request().Header.SetContentLength(len(gzipJSON.Bytes()))
c.Request().Header.Set(fasthttp.HeaderContentEncoding, encoding)
c.Request().SetBody(compressedBody)
c.Request().Header.SetContentLength(len(compressedBody))
d := new(Demo)
require.NoError(t, c.Bind().Body(d))
require.Equal(t, "john", d.Name)
c.Request().Header.Del(HeaderContentEncoding)
c.Request().Header.Del(fasthttp.HeaderContentEncoding)
}

testDecodeParser := func(contentType, body string) {
t.Run("Gzip", func(t *testing.T) {
t.Parallel()
compressedBody := fasthttp.AppendGzipBytes(nil, reqBody)
require.NotEqual(t, reqBody, compressedBody)
testCompressedBody(t, compressedBody, "gzip")
})

t.Run("Deflate", func(t *testing.T) {
t.Parallel()
compressedBody := fasthttp.AppendDeflateBytes(nil, reqBody)
require.NotEqual(t, reqBody, compressedBody)
testCompressedBody(t, compressedBody, "deflate")
})

t.Run("Brotli", func(t *testing.T) {
t.Parallel()
compressedBody := fasthttp.AppendBrotliBytes(nil, reqBody)
require.NotEqual(t, reqBody, compressedBody)
testCompressedBody(t, compressedBody, "br")
})

t.Run("Zstd", func(t *testing.T) {
t.Parallel()
compressedBody := fasthttp.AppendZstdBytes(nil, reqBody)
require.NotEqual(t, reqBody, compressedBody)
testCompressedBody(t, compressedBody, "zstd")
})

testDecodeParser := func(t *testing.T, contentType, body string) {
t.Helper()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.SetContentType(contentType)
c.Request().SetBody([]byte(body))
c.Request().Header.SetContentLength(len(body))
Expand All @@ -861,44 +887,68 @@ func Test_Bind_Body(t *testing.T) {
require.Equal(t, "john", d.Name)
}

testDecodeParser(MIMEApplicationJSON, `{"name":"john"}`)
testDecodeParser(MIMEApplicationXML, `<Demo><name>john</name></Demo>`)
testDecodeParser(MIMEApplicationForm, "name=john")
testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--")
t.Run("JSON", func(t *testing.T) {
testDecodeParser(t, MIMEApplicationJSON, `{"name":"john"}`)
})

t.Run("XML", func(t *testing.T) {
testDecodeParser(t, MIMEApplicationXML, `<Demo><name>john</name></Demo>`)
})

t.Run("Form", func(t *testing.T) {
testDecodeParser(t, MIMEApplicationForm, "name=john")
})

testDecodeParserError := func(contentType, body string) {
t.Run("MultipartForm", func(t *testing.T) {
testDecodeParser(t, MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--")
})

testDecodeParserError := func(t *testing.T, contentType, body string) {
t.Helper()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.SetContentType(contentType)
c.Request().SetBody([]byte(body))
c.Request().Header.SetContentLength(len(body))
require.Error(t, c.Bind().Body(nil))
}

testDecodeParserError("invalid-content-type", "")
testDecodeParserError(MIMEMultipartForm+`;boundary="b"`, "--b")
t.Run("ErrorInvalidContentType", func(t *testing.T) {
testDecodeParserError(t, "invalid-content-type", "")
})

t.Run("ErrorMalformedMultipart", func(t *testing.T) {
testDecodeParserError(t, MIMEMultipartForm+`;boundary="b"`, "--b")
})

type CollectionQuery struct {
Data []Demo `query:"data"`
}

c.Request().Reset()
c.Request().Header.SetContentType(MIMEApplicationForm)
c.Request().SetBody([]byte("data[0][name]=john&data[1][name]=doe"))
c.Request().Header.SetContentLength(len(c.Body()))
cq := new(CollectionQuery)
require.NoError(t, c.Bind().Body(cq))
require.Len(t, cq.Data, 2)
require.Equal(t, "john", cq.Data[0].Name)
require.Equal(t, "doe", cq.Data[1].Name)
t.Run("CollectionQuerySquareBrackets", func(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Reset()
c.Request().Header.SetContentType(MIMEApplicationForm)
c.Request().SetBody([]byte("data[0][name]=john&data[1][name]=doe"))
c.Request().Header.SetContentLength(len(c.Body()))
cq := new(CollectionQuery)
require.NoError(t, c.Bind().Body(cq))
require.Len(t, cq.Data, 2)
require.Equal(t, "john", cq.Data[0].Name)
require.Equal(t, "doe", cq.Data[1].Name)
})

c.Request().Reset()
c.Request().Header.SetContentType(MIMEApplicationForm)
c.Request().SetBody([]byte("data.0.name=john&data.1.name=doe"))
c.Request().Header.SetContentLength(len(c.Body()))
cq = new(CollectionQuery)
require.NoError(t, c.Bind().Body(cq))
require.Len(t, cq.Data, 2)
require.Equal(t, "john", cq.Data[0].Name)
require.Equal(t, "doe", cq.Data[1].Name)
t.Run("CollectionQueryDotNotation", func(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Reset()
c.Request().Header.SetContentType(MIMEApplicationForm)
c.Request().SetBody([]byte("data.0.name=john&data.1.name=doe"))
c.Request().Header.SetContentLength(len(c.Body()))
cq := new(CollectionQuery)
require.NoError(t, c.Bind().Body(cq))
require.Len(t, cq.Data, 2)
require.Equal(t, "john", cq.Data[0].Name)
require.Equal(t, "doe", cq.Data[1].Name)
})
}

// go test -run Test_Bind_Body_WithSetParserDecoder
Expand Down
1 change: 1 addition & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ const (
StrBr = "br"
StrDeflate = "deflate"
StrBrotli = "brotli"
StrZstd = "zstd"
)

// Cookie SameSite
Expand Down
19 changes: 11 additions & 8 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ func (c *DefaultCtx) tryDecodeBodyInOrder(
body, err = c.fasthttp.Request.BodyUnbrotli()
case StrDeflate:
body, err = c.fasthttp.Request.BodyInflate()
case StrZstd:
body, err = c.fasthttp.Request.BodyUnzstd()
default:
decodesRealized--
if len(encodings) == 1 {
Expand Down Expand Up @@ -1429,14 +1431,15 @@ func (c *DefaultCtx) SendFile(file string, compress ...bool) error {
sendFileOnce.Do(func() {
const cacheDuration = 10 * time.Second
sendFileFS = &fasthttp.FS{
Root: "",
AllowEmptyRoot: true,
GenerateIndexPages: false,
AcceptByteRange: true,
Compress: true,
CompressedFileSuffix: c.app.config.CompressedFileSuffix,
CacheDuration: cacheDuration,
IndexNames: []string{"index.html"},
Root: "",
AllowEmptyRoot: true,
GenerateIndexPages: false,
AcceptByteRange: true,
Compress: true,
CompressBrotli: true,
CompressedFileSuffixes: c.app.config.CompressedFileSuffixes,
CacheDuration: cacheDuration,
IndexNames: []string{"index.html"},
PathNotFound: func(ctx *fasthttp.RequestCtx) {
ctx.Response.SetStatusCode(StatusNotFound)
},
Expand Down
Loading

0 comments on commit b9936a3

Please sign in to comment.