diff --git a/bind.go b/bind.go
index 747a7694e7a..1b9514a687c 100644
--- a/bind.go
+++ b/bind.go
@@ -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)
diff --git a/binder/form.go b/binder/form.go
index 81543b133c7..c93102ab1c9 100644
--- a/binder/form.go
+++ b/binder/form.go
@@ -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.
@@ -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
}
@@ -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 {
diff --git a/binder/form_test.go b/binder/form_test.go
index a8cb68508b4..68ab4ffcee4 100644
--- a/binder/form_test.go
+++ b/binder/form_test.go
@@ -2,8 +2,10 @@ package binder
import (
"bytes"
+ "compress/gzip"
"io"
"mime/multipart"
+ "strings"
"testing"
"github.com/stretchr/testify/require"
@@ -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()
diff --git a/ctx_test.go b/ctx_test.go
index acebd962d5f..7190f2b9662 100644
--- a/ctx_test.go
+++ b/ctx_test.go
@@ -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")
@@ -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{})
@@ -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()
diff --git a/docs/api/ctx.md b/docs/api/ctx.md
index a4941254299..61a8492ec03 100644
--- a/docs/api/ctx.md
+++ b/docs/api/ctx.md
@@ -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
@@ -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)
diff --git a/docs/api/fiber.md b/docs/api/fiber.md
index 0b61b7115c1..1278a10de83 100644
--- a/docs/api/fiber.md
+++ b/docs/api/fiber.md
@@ -45,7 +45,7 @@ app := fiber.New(fiber.Config{
| Property | Type | Description | Default |
|---------------------------------------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| AppName | `string` | Sets the application name used in logs and the Server header | `""` |
-| BodyLimit | `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` |
+| BodyLimit | `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` |
| CaseSensitive | `bool` | When enabled, `/Foo` and `/foo` are different routes. When disabled, `/Foo` and `/foo` are treated the same. | `false` |
| CBORDecoder | `utils.CBORUnmarshal` | Allowing for flexibility in using another cbor library for decoding. | `binder.UnimplementedCborUnmarshal` |
| CBOREncoder | `utils.CBORMarshal` | Allowing for flexibility in using another cbor library for encoding. | `binder.UnimplementedCborMarshal` |
diff --git a/docs/middleware/compress.md b/docs/middleware/compress.md
index 339aba4046a..beae7661bca 100644
--- a/docs/middleware/compress.md
+++ b/docs/middleware/compress.md
@@ -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
diff --git a/docs/whats_new.md b/docs/whats_new.md
index 97df46f5bd1..c98c453f989 100644
--- a/docs/whats_new.md
+++ b/docs/whats_new.md
@@ -1403,6 +1403,8 @@ Additionally, panic messages and logs redact misconfigured origins by default, a
- Compression is bypassed for responses that already specify `Content-Encoding`, for range requests or `206` statuses, and when either side sends `Cache-Control: no-transform`.
- `HEAD` requests still negotiate compression so `Content-Encoding`, `Content-Length`, `ETag`, and `Vary` match a corresponding `GET`, but the body is omitted.
- `Vary: Accept-Encoding` is merged into responses even when compression is skipped, preventing caches from mixing encoded and unencoded variants.
+- Decoding compressed request bodies now enforces the app `BodyLimit` through fasthttp `WithLimit` helpers, including when the compression middleware is active.
+- Multipart form parsing now enforces the app `BodyLimit` by using fasthttp `MultipartFormWithLimit`.
### CSRF
diff --git a/middleware/compress/compress_test.go b/middleware/compress/compress_test.go
index 9aa7603e536..3a20312484a 100644
--- a/middleware/compress/compress_test.go
+++ b/middleware/compress/compress_test.go
@@ -1,12 +1,15 @@
package compress
import (
+ "bytes"
+ "compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
+ "strings"
"testing"
"time"
@@ -245,6 +248,40 @@ func Test_Compress_Vary_List_Star(t *testing.T) {
require.Equal(t, "User-Agent, *", resp.Header.Get(fiber.HeaderVary))
}
+func Test_Compress_RespectsBodyLimitOnCompressedRequestBody(t *testing.T) {
+ t.Parallel()
+
+ app := fiber.New(fiber.Config{
+ BodyLimit: 40,
+ })
+
+ app.Use(New())
+
+ app.Post("/", func(c fiber.Ctx) error {
+ return c.Send(c.Body())
+ })
+
+ var b bytes.Buffer
+ gz := gzip.NewWriter(&b)
+ _, err := gz.Write([]byte(strings.Repeat("a", 256)))
+ require.NoError(t, err)
+ require.NoError(t, gz.Flush())
+ require.NoError(t, gz.Close())
+
+ req := httptest.NewRequest(fiber.MethodPost, "/", bytes.NewReader(b.Bytes()))
+ req.Header.Set(fiber.HeaderContentEncoding, fiber.StrGzip)
+ req.Header.Set(fiber.HeaderContentType, fiber.MIMETextPlain)
+ req.Header.Set(fiber.HeaderAcceptEncoding, fiber.StrIdentity)
+
+ resp, err := app.Test(req, testConfig)
+ require.NoError(t, err)
+ require.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode)
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Equal(t, fasthttp.ErrBodyTooLarge.Error(), string(body))
+}
+
func Test_Compress_Vary_Similar_Substring(t *testing.T) {
t.Parallel()
app := fiber.New()
diff --git a/req.go b/req.go
index ebe861d8bde..cc4116f9583 100644
--- a/req.go
+++ b/req.go
@@ -102,6 +102,7 @@ func (r *DefaultReq) tryDecodeBodyInOrder(
encodings []string,
) (body []byte, decodesRealized uint8, err error) {
request := &r.c.fasthttp.Request
+ maxBodySize := r.c.app.config.BodyLimit
for idx := range encodings {
i := len(encodings) - 1 - idx
encoding := encodings[i]
@@ -109,13 +110,13 @@ func (r *DefaultReq) tryDecodeBodyInOrder(
var decodeErr error
switch encoding {
case StrGzip, "x-gzip":
- body, decodeErr = request.BodyGunzip()
+ body, decodeErr = request.BodyGunzipWithLimit(maxBodySize)
case StrBr, StrBrotli:
- body, decodeErr = request.BodyUnbrotli()
+ body, decodeErr = request.BodyUnbrotliWithLimit(maxBodySize)
case StrDeflate:
- body, decodeErr = request.BodyInflate()
+ body, decodeErr = request.BodyInflateWithLimit(maxBodySize)
case StrZstd:
- body, decodeErr = request.BodyUnzstd()
+ body, decodeErr = request.BodyUnzstdWithLimit(maxBodySize)
case StrIdentity:
body = request.Body()
case StrCompress, "x-compress":
@@ -187,6 +188,8 @@ func (r *DefaultReq) Body() []byte {
_ = r.c.DefaultRes.SendStatus(StatusUnsupportedMediaType) //nolint:errcheck,staticcheck // It is fine to ignore the error and the static check
case errors.Is(err, ErrNotImplemented):
_ = r.c.DefaultRes.SendStatus(StatusNotImplemented) //nolint:errcheck,staticcheck // It is fine to ignore the error and the static check
+ case errors.Is(err, fasthttp.ErrBodyTooLarge):
+ _ = r.c.DefaultRes.SendStatus(StatusRequestEntityTooLarge) //nolint:errcheck,staticcheck // It is fine to ignore the error and the static check
default:
// do nothing
}
@@ -350,7 +353,12 @@ func (r *DefaultReq) Request() *fasthttp.Request {
}
// FormFile returns the first file by key from a MultipartForm.
+// The multipart form is parsed using the application's BodyLimit to prevent
+// unbounded memory usage.
func (r *DefaultReq) FormFile(key string) (*multipart.FileHeader, error) {
+ if _, err := r.MultipartForm(); err != nil {
+ return nil, err
+ }
return r.c.fasthttp.FormFile(key)
}
@@ -360,7 +368,30 @@ func (r *DefaultReq) FormFile(key string) (*multipart.FileHeader, error) {
// If a default value is given, it will return that value if the form value does not exist.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting instead.
+// When the request is a multipart form, it is parsed using the application's
+// BodyLimit so the configured limit is consistently enforced.
func (r *DefaultReq) FormValue(key string, defaultValue ...string) string {
+ if r.c.IsMultipart() {
+ // For multipart requests, parse the form using the application's BodyLimit.
+ // fasthttp's FormValue would otherwise re-parse with its default 8 MiB limit,
+ // effectively bypassing the configured BodyLimit.
+ //
+ // Preserve the original search order: QueryArgs → PostArgs → MultipartForm.
+ if v := r.c.fasthttp.QueryArgs().Peek(key); len(v) > 0 {
+ return r.c.app.toString(v)
+ }
+ if v := r.c.fasthttp.PostArgs().Peek(key); len(v) > 0 {
+ return r.c.app.toString(v)
+ }
+ mf, err := r.MultipartForm()
+ if err != nil {
+ return defaultString("", defaultValue)
+ }
+ if vals := mf.Value[key]; len(vals) > 0 {
+ return vals[0]
+ }
+ return defaultString("", defaultValue)
+ }
return defaultString(r.c.app.toString(r.c.fasthttp.FormValue(key)), defaultValue)
}
@@ -721,7 +752,7 @@ func (r *DefaultReq) Method(override ...string) string {
// MultipartForm parse form entries from binary.
// This returns a map[string][]string, so given a key, the value will be a string slice.
func (r *DefaultReq) MultipartForm() (*multipart.Form, error) {
- return r.c.fasthttp.MultipartForm()
+ return r.c.fasthttp.MultipartFormWithLimit(r.c.app.config.BodyLimit)
}
// OriginalURL contains the original request URL.