From 098811c8467c188e5416b837b47d93c857263884 Mon Sep 17 00:00:00 2001
From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>
Date: Tue, 14 Apr 2026 07:52:20 -0400
Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=90=9B=20bug:=20enforce=20multipart?=
=?UTF-8?q?=20parsing=20limits=20with=20BodyLimit?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
bind.go | 1 +
binder/form.go | 4 ++-
binder/form_test.go | 39 ++++++++++++++++++++
ctx_test.go | 54 ++++++++++++++++++++++++++++
docs/api/ctx.md | 4 +--
docs/api/fiber.md | 2 +-
docs/middleware/compress.md | 1 +
docs/whats_new.md | 2 ++
middleware/compress/compress_test.go | 37 +++++++++++++++++++
req.go | 11 +++---
10 files changed, 146 insertions(+), 9 deletions(-)
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..3b4c16af1d9 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, StatusOK, 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")
@@ -3930,6 +3956,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 20c7781d092..38c2fbbd588 100644
--- a/docs/whats_new.md
+++ b/docs/whats_new.md
@@ -1401,6 +1401,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..8c1f9ce369c 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.StatusOK, 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..16b0fc81cdb 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":
@@ -721,7 +722,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.
From fafaf8f45f4789d49f7c9fdd766981c48fc9a4ac Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Apr 2026 12:55:36 +0000
Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=90=9B=20fix:=20return=20413=20when?=
=?UTF-8?q?=20BodyLimit=20exceeded=20during=20decompression=20in=20Ctx.Bod?=
=?UTF-8?q?y()?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/22fadb4e-293c-469f-a26a-3ecc62c4c5b0
Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
---
ctx_test.go | 2 +-
req.go | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/ctx_test.go b/ctx_test.go
index 3b4c16af1d9..00ca9520bb9 100644
--- a/ctx_test.go
+++ b/ctx_test.go
@@ -1047,7 +1047,7 @@ func Test_Ctx_Body_With_Compression_BodyLimitExceeded(t *testing.T) {
body := c.Body()
require.Equal(t, []byte(fasthttp.ErrBodyTooLarge.Error()), body)
require.Equal(t, compressedBody, c.Request().Body())
- require.Equal(t, StatusOK, c.Response().StatusCode())
+ require.Equal(t, StatusRequestEntityTooLarge, c.Response().StatusCode())
}
// go test -v -run=^$ -bench=Benchmark_Ctx_Body_With_Compression -benchmem -count=4
diff --git a/req.go b/req.go
index 16b0fc81cdb..2c02d74c27b 100644
--- a/req.go
+++ b/req.go
@@ -188,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
}
From ad614ebb6a2ff4c2491d1497b5fde8f655f0aad5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Apr 2026 13:11:24 +0000
Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=90=9B=20fix:=20update=20compress=20m?=
=?UTF-8?q?iddleware=20test=20to=20expect=20413=20on=20body=20limit=20exce?=
=?UTF-8?q?eded?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/76b45b30-ff6b-4e33-b479-aa68389efec5
Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
---
middleware/compress/compress_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/middleware/compress/compress_test.go b/middleware/compress/compress_test.go
index 8c1f9ce369c..3a20312484a 100644
--- a/middleware/compress/compress_test.go
+++ b/middleware/compress/compress_test.go
@@ -275,7 +275,7 @@ func Test_Compress_RespectsBodyLimitOnCompressedRequestBody(t *testing.T) {
resp, err := app.Test(req, testConfig)
require.NoError(t, err)
- require.Equal(t, fiber.StatusOK, resp.StatusCode)
+ require.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
From 97efe653bced91cb4c0c2541fb6fab6ca1e6de87 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Apr 2026 13:26:44 +0000
Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=90=9B=20fix:=20enforce=20BodyLimit?=
=?UTF-8?q?=20in=20FormFile=20and=20FormValue?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/5b86c6bd-c9af-4bf0-a36b-f5d170af9c47
Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
---
ctx_test.go | 45 ++++++++++++++++++++++++++++++++++++++++++++-
req.go | 28 ++++++++++++++++++++++++++++
2 files changed, 72 insertions(+), 1 deletion(-)
diff --git a/ctx_test.go b/ctx_test.go
index 00ca9520bb9..7aed9d30e5f 100644
--- a/ctx_test.go
+++ b/ctx_test.go
@@ -2262,7 +2262,50 @@ 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)
+}
+
func Benchmark_Ctx_Fresh_StaleEtag(b *testing.B) {
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
diff --git a/req.go b/req.go
index 2c02d74c27b..cc4116f9583 100644
--- a/req.go
+++ b/req.go
@@ -353,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)
}
@@ -363,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)
}
From 1b232b547e913d179ec2e3e1c86c24d882137f54 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Apr 2026 14:19:02 +0000
Subject: [PATCH 5/5] test: add 100% coverage for FormValue in ctx_test.go
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/1de7bad0-0284-44e5-b05f-ce7837e40ca5
Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
---
ctx_test.go | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 79 insertions(+)
diff --git a/ctx_test.go b/ctx_test.go
index 7aed9d30e5f..7190f2b9662 100644
--- a/ctx_test.go
+++ b/ctx_test.go
@@ -2306,6 +2306,85 @@ func Test_Ctx_FormValue_BodyLimitExceeded(t *testing.T) {
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{})