Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ func (b *Bind) XML(out any) error {
func (b *Bind) Form(out any) error {
bind := binder.GetFromThePool[*binder.FormBinding](&binder.FormBinderPool)
bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
bind.MaxBodySize = b.ctx.App().config.BodyLimit

defer releasePooledBinder(&binder.FormBinderPool, bind)

Expand Down
4 changes: 3 additions & 1 deletion binder/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var (
// FormBinding is the form binder for form request body.
type FormBinding struct {
EnableSplitting bool
MaxBodySize int
}

// Name returns the binding name.
Expand Down Expand Up @@ -56,7 +57,7 @@ func (b *FormBinding) Bind(req *fasthttp.Request, out any) error {

// bindMultipart parses the request body and returns the result.
func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error {
multipartForm, err := req.MultipartForm()
multipartForm, err := req.MultipartFormWithLimit(b.MaxBodySize)
if err != nil {
return err
}
Expand Down Expand Up @@ -87,6 +88,7 @@ func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error {
// Reset resets the FormBinding binder.
func (b *FormBinding) Reset() {
b.EnableSplitting = false
b.MaxBodySize = 0
}

func acquireFormMap() map[string][]string {
Expand Down
39 changes: 39 additions & 0 deletions binder/form_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package binder

import (
"bytes"
"compress/gzip"
"io"
"mime/multipart"
"strings"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -205,6 +207,43 @@ func Test_FormBinder_BindMultipart(t *testing.T) {
require.Equal(t, "avatar2", string(content))
}

func Test_FormBinder_BindMultipart_BodyLimitExceeded(t *testing.T) {
t.Parallel()

b := &FormBinding{
MaxBodySize: 64,
}

type User struct {
Name string `form:"name"`
}
var user User

req := fasthttp.AcquireRequest()
t.Cleanup(func() {
fasthttp.ReleaseRequest(req)
})

multipartBody := &bytes.Buffer{}
mw := multipart.NewWriter(multipartBody)
require.NoError(t, mw.WriteField("name", strings.Repeat("a", 1024)))
require.NoError(t, mw.Close())

var compressed bytes.Buffer
gz := gzip.NewWriter(&compressed)
_, err := gz.Write(multipartBody.Bytes())
require.NoError(t, err)
require.NoError(t, gz.Flush())
require.NoError(t, gz.Close())

req.Header.SetContentType(mw.FormDataContentType())
req.Header.SetContentEncoding("gzip")
req.SetBody(compressed.Bytes())

err = b.Bind(req, &user)
require.ErrorIs(t, err, fasthttp.ErrBodyTooLarge)
}

func Test_FormBinder_BindMultipart_ValueError(t *testing.T) {
b := &FormBinding{}
req := fasthttp.AcquireRequest()
Expand Down
178 changes: 177 additions & 1 deletion ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,32 @@ func Test_Ctx_Body_With_Compression(t *testing.T) {
}
}

func Test_Ctx_Body_With_Compression_BodyLimitExceeded(t *testing.T) {
t.Parallel()

app := New(Config{
BodyLimit: 8,
})

c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().Header.Set(HeaderContentEncoding, StrGzip)

var b bytes.Buffer
gz := gzip.NewWriter(&b)
_, err := gz.Write([]byte("payload-over-limit"))
require.NoError(t, err)
require.NoError(t, gz.Flush())
require.NoError(t, gz.Close())

compressedBody := b.Bytes()
c.Request().SetBody(compressedBody)

body := c.Body()
require.Equal(t, []byte(fasthttp.ErrBodyTooLarge.Error()), body)
require.Equal(t, compressedBody, c.Request().Body())
require.Equal(t, StatusRequestEntityTooLarge, c.Response().StatusCode())
}

// go test -v -run=^$ -bench=Benchmark_Ctx_Body_With_Compression -benchmem -count=4
func Benchmark_Ctx_Body_With_Compression(b *testing.B) {
encodingErr := errors.New("failed to encoding data")
Expand Down Expand Up @@ -2236,7 +2262,129 @@ func Test_Ctx_FormValue(t *testing.T) {
require.Equal(t, int64(0), resp.ContentLength)
}

// go test -v -run=^$ -bench=Benchmark_Ctx_Fresh_StaleEtag -benchmem -count=4
func Test_Ctx_FormFile_BodyLimitExceeded(t *testing.T) {
t.Parallel()

app := New(Config{
BodyLimit: 64,
})
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed

body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
ioWriter, err := writer.CreateFormFile("file", "test.txt")
require.NoError(t, err)
_, err = ioWriter.Write([]byte(strings.Repeat("a", 256)))
require.NoError(t, err)
require.NoError(t, writer.Close())

c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
c.Request().SetBody(body.Bytes())

_, err = c.FormFile("file")
require.ErrorIs(t, err, fasthttp.ErrBodyTooLarge)
}

func Test_Ctx_FormValue_BodyLimitExceeded(t *testing.T) {
t.Parallel()

app := New(Config{
BodyLimit: 64,
})
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed

body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
require.NoError(t, writer.WriteField("name", strings.Repeat("a", 256)))
require.NoError(t, writer.Close())

c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
c.Request().SetBody(body.Bytes())

// FormValue should return empty string (default) when the body limit is exceeded.
val := c.FormValue("name")
require.Empty(t, val)
}

// Test_Ctx_FormValue_QueryArgs covers the path where the key is found in QueryArgs
// for a multipart request, which is checked before the multipart form is parsed.
func Test_Ctx_FormValue_QueryArgs(t *testing.T) {
t.Parallel()

app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed

body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
require.NoError(t, writer.WriteField("other", "value"))
require.NoError(t, writer.Close())

c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
c.Request().SetBody(body.Bytes())
// Set the key in QueryArgs so the QueryArgs branch returns it.
c.Request().URI().QueryArgs().Set("name", "alice")

require.Equal(t, "alice", c.FormValue("name"))
}

// Test_Ctx_FormValue_PostArgs covers the path where the key is found in PostArgs
// for a multipart request, which is checked before the multipart form is parsed.
func Test_Ctx_FormValue_PostArgs(t *testing.T) {
t.Parallel()

app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed

body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
require.NoError(t, writer.WriteField("other", "value"))
require.NoError(t, writer.Close())

c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
c.Request().SetBody(body.Bytes())
// Manually set a PostArg so that the PostArgs branch returns it.
c.Request().PostArgs().Set("name", "bob")

require.Equal(t, "bob", c.FormValue("name"))
}

// Test_Ctx_FormValue_MultipartKeyNotFound covers the path where the key is absent
// from the multipart form: returns "" without a default and the provided default otherwise.
func Test_Ctx_FormValue_MultipartKeyNotFound(t *testing.T) {
t.Parallel()

app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed

body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
require.NoError(t, writer.WriteField("other", "value"))
require.NoError(t, writer.Close())

c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
c.Request().SetBody(body.Bytes())

// Key not present → empty string.
require.Empty(t, c.FormValue("missing"))
// Key not present + default → default value.
require.Equal(t, "fallback", c.FormValue("missing", "fallback"))
}

// Test_Ctx_FormValue_NonMultipart covers the non-multipart branch (e.g. URL-encoded body).
func Test_Ctx_FormValue_NonMultipart(t *testing.T) {
t.Parallel()

app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed

c.Request().Header.Set(HeaderContentType, MIMEApplicationForm)
c.Request().SetBodyString("name=carol")

require.Equal(t, "carol", c.FormValue("name"))
// Key not present + default → default value.
require.Equal(t, "fallback", c.FormValue("missing", "fallback"))
}

func Benchmark_Ctx_Fresh_StaleEtag(b *testing.B) {
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
Expand Down Expand Up @@ -3930,6 +4078,34 @@ func Test_Ctx_MultipartForm(t *testing.T) {
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
}

func Test_Ctx_MultipartForm_BodyLimitExceeded(t *testing.T) {
t.Parallel()

app := New(Config{
BodyLimit: 64,
})
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed

multipartBody := &bytes.Buffer{}
writer := multipart.NewWriter(multipartBody)
require.NoError(t, writer.WriteField("name", strings.Repeat("a", 1024)))
require.NoError(t, writer.Close())

var compressed bytes.Buffer
gz := gzip.NewWriter(&compressed)
_, err := gz.Write(multipartBody.Bytes())
require.NoError(t, err)
require.NoError(t, gz.Flush())
require.NoError(t, gz.Close())

c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
c.Request().Header.Set(HeaderContentEncoding, StrGzip)
c.Request().SetBody(compressed.Bytes())

_, err = c.MultipartForm()
require.ErrorIs(t, err, fasthttp.ErrBodyTooLarge)
}

// go test -v -run=^$ -bench=Benchmark_Ctx_MultipartForm -benchmem -count=4
func Benchmark_Ctx_MultipartForm(b *testing.B) {
app := New()
Expand Down
4 changes: 2 additions & 2 deletions docs/api/ctx.md
Original file line number Diff line number Diff line change
Expand Up @@ -869,7 +869,7 @@ app.Get("/", func(c fiber.Ctx) error {

### Body

As per the header `Content-Encoding`, this method will try to perform a file decompression from the **body** bytes. In case no `Content-Encoding` header is sent (or when it is set to `identity`), it will perform as [BodyRaw](#bodyraw). If an unknown or unsupported encoding is encountered, the response status will be `415 Unsupported Media Type` or `501 Not Implemented`.
As per the header `Content-Encoding`, this method will try to perform a file decompression from the **body** bytes. In case no `Content-Encoding` header is sent (or when it is set to `identity`), it will perform as [BodyRaw](#bodyraw). If an unknown or unsupported encoding is encountered, the response status will be `415 Unsupported Media Type` or `501 Not Implemented`. Decompression is bounded by the app [BodyLimit](./fiber.md#bodylimit).

```go title="Signature"
func (c fiber.Ctx) Body() []byte
Expand Down Expand Up @@ -1368,7 +1368,7 @@ app.Post("/override", func(c fiber.Ctx) error {

### MultipartForm

To access multipart form entries, you can parse the binary with `MultipartForm()`. This returns a `*multipart.Form`, allowing you to access form values and files.
To access multipart form entries, you can parse the binary with `MultipartForm()`. This returns a `*multipart.Form`, allowing you to access form values and files. Parsing is bounded by the app [BodyLimit](./fiber.md#bodylimit).

```go title="Signature"
func (c fiber.Ctx) MultipartForm() (*multipart.Form, error)
Expand Down
2 changes: 1 addition & 1 deletion docs/api/fiber.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ app := fiber.New(fiber.Config{
| Property | Type | Description | Default |
|---------------------------------------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| <Reference id="appname">AppName</Reference> | `string` | Sets the application name used in logs and the Server header | `""` |
| <Reference id="bodylimit">BodyLimit</Reference> | `int` | Sets the maximum allowed size for a request body. Zero or negative values fall back to the default limit. If the size exceeds the configured limit, it sends `413 - Request Entity Too Large` response. This limit also applies when running Fiber through the adaptor middleware from `net/http`. | `4 * 1024 * 1024` |
| <Reference id="bodylimit">BodyLimit</Reference> | `int` | Sets the maximum allowed size for a request body. Zero or negative values fall back to the default limit. If the size exceeds the configured limit, it sends `413 - Request Entity Too Large` response. This limit also applies when running Fiber through the adaptor middleware from `net/http`, when decoding compressed request bodies via [`Ctx.Body()`](./ctx.md#body), and when parsing multipart form data via [`Ctx.MultipartForm()`](./ctx.md#multipartform). | `4 * 1024 * 1024` |
| <Reference id="casesensitive">CaseSensitive</Reference> | `bool` | When enabled, `/Foo` and `/foo` are different routes. When disabled, `/Foo` and `/foo` are treated the same. | `false` |
| <Reference id="cbordecoder">CBORDecoder</Reference> | `utils.CBORUnmarshal` | Allowing for flexibility in using another cbor library for decoding. | `binder.UnimplementedCborUnmarshal` |
| <Reference id="cborencoder">CBOREncoder</Reference> | `utils.CBORMarshal` | Allowing for flexibility in using another cbor library for encoding. | `binder.UnimplementedCborMarshal` |
Expand Down
1 change: 1 addition & 0 deletions docs/middleware/compress.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Bodies smaller than 200 bytes remain uncompressed because compression would like
- Skips compression for responses that already define `Content-Encoding`, for range requests, `206` responses, status codes without bodies, or when either side sends `Cache-Control: no-transform`.
- `HEAD` requests negotiate compression so `Content-Encoding`, `Content-Length`, `ETag`, and `Vary` reflect the encoded representation, but the body is removed before sending.
- When compression runs, strong `ETag` values are recomputed from the compressed bytes; when skipped, `Accept-Encoding` is still merged into `Vary` unless the header is `*` or already present.
- Request-body decompression is still handled by Fiber's request APIs (for example `c.Body()`), and those decoders enforce the app `BodyLimit` for compressed payloads.

## Signatures

Expand Down
Loading
Loading