Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔥 Feature: Add support for zstd compression #3041

Merged
merged 6 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
gaby marked this conversation as resolved.
Show resolved Hide resolved

// 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
Loading