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{})