Skip to content

Commit a04daa2

Browse files
authored
Merge pull request #4213 from gofiber/update-fasthttp-compression-to-use-withlimit
2 parents b08638a + 1b232b5 commit a04daa2

File tree

10 files changed

+299
-10
lines changed

10 files changed

+299
-10
lines changed

bind.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ func (b *Bind) XML(out any) error {
337337
func (b *Bind) Form(out any) error {
338338
bind := binder.GetFromThePool[*binder.FormBinding](&binder.FormBinderPool)
339339
bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
340+
bind.MaxBodySize = b.ctx.App().config.BodyLimit
340341

341342
defer releasePooledBinder(&binder.FormBinderPool, bind)
342343

binder/form.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var (
2626
// FormBinding is the form binder for form request body.
2727
type FormBinding struct {
2828
EnableSplitting bool
29+
MaxBodySize int
2930
}
3031

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

5758
// bindMultipart parses the request body and returns the result.
5859
func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error {
59-
multipartForm, err := req.MultipartForm()
60+
multipartForm, err := req.MultipartFormWithLimit(b.MaxBodySize)
6061
if err != nil {
6162
return err
6263
}
@@ -87,6 +88,7 @@ func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error {
8788
// Reset resets the FormBinding binder.
8889
func (b *FormBinding) Reset() {
8990
b.EnableSplitting = false
91+
b.MaxBodySize = 0
9092
}
9193

9294
func acquireFormMap() map[string][]string {

binder/form_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package binder
22

33
import (
44
"bytes"
5+
"compress/gzip"
56
"io"
67
"mime/multipart"
8+
"strings"
79
"testing"
810

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

210+
func Test_FormBinder_BindMultipart_BodyLimitExceeded(t *testing.T) {
211+
t.Parallel()
212+
213+
b := &FormBinding{
214+
MaxBodySize: 64,
215+
}
216+
217+
type User struct {
218+
Name string `form:"name"`
219+
}
220+
var user User
221+
222+
req := fasthttp.AcquireRequest()
223+
t.Cleanup(func() {
224+
fasthttp.ReleaseRequest(req)
225+
})
226+
227+
multipartBody := &bytes.Buffer{}
228+
mw := multipart.NewWriter(multipartBody)
229+
require.NoError(t, mw.WriteField("name", strings.Repeat("a", 1024)))
230+
require.NoError(t, mw.Close())
231+
232+
var compressed bytes.Buffer
233+
gz := gzip.NewWriter(&compressed)
234+
_, err := gz.Write(multipartBody.Bytes())
235+
require.NoError(t, err)
236+
require.NoError(t, gz.Flush())
237+
require.NoError(t, gz.Close())
238+
239+
req.Header.SetContentType(mw.FormDataContentType())
240+
req.Header.SetContentEncoding("gzip")
241+
req.SetBody(compressed.Bytes())
242+
243+
err = b.Bind(req, &user)
244+
require.ErrorIs(t, err, fasthttp.ErrBodyTooLarge)
245+
}
246+
208247
func Test_FormBinder_BindMultipart_ValueError(t *testing.T) {
209248
b := &FormBinding{}
210249
req := fasthttp.AcquireRequest()

ctx_test.go

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,32 @@ func Test_Ctx_Body_With_Compression(t *testing.T) {
10241024
}
10251025
}
10261026

1027+
func Test_Ctx_Body_With_Compression_BodyLimitExceeded(t *testing.T) {
1028+
t.Parallel()
1029+
1030+
app := New(Config{
1031+
BodyLimit: 8,
1032+
})
1033+
1034+
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
1035+
c.Request().Header.Set(HeaderContentEncoding, StrGzip)
1036+
1037+
var b bytes.Buffer
1038+
gz := gzip.NewWriter(&b)
1039+
_, err := gz.Write([]byte("payload-over-limit"))
1040+
require.NoError(t, err)
1041+
require.NoError(t, gz.Flush())
1042+
require.NoError(t, gz.Close())
1043+
1044+
compressedBody := b.Bytes()
1045+
c.Request().SetBody(compressedBody)
1046+
1047+
body := c.Body()
1048+
require.Equal(t, []byte(fasthttp.ErrBodyTooLarge.Error()), body)
1049+
require.Equal(t, compressedBody, c.Request().Body())
1050+
require.Equal(t, StatusRequestEntityTooLarge, c.Response().StatusCode())
1051+
}
1052+
10271053
// go test -v -run=^$ -bench=Benchmark_Ctx_Body_With_Compression -benchmem -count=4
10281054
func Benchmark_Ctx_Body_With_Compression(b *testing.B) {
10291055
encodingErr := errors.New("failed to encoding data")
@@ -2236,7 +2262,129 @@ func Test_Ctx_FormValue(t *testing.T) {
22362262
require.Equal(t, int64(0), resp.ContentLength)
22372263
}
22382264

2239-
// go test -v -run=^$ -bench=Benchmark_Ctx_Fresh_StaleEtag -benchmem -count=4
2265+
func Test_Ctx_FormFile_BodyLimitExceeded(t *testing.T) {
2266+
t.Parallel()
2267+
2268+
app := New(Config{
2269+
BodyLimit: 64,
2270+
})
2271+
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
2272+
2273+
body := &bytes.Buffer{}
2274+
writer := multipart.NewWriter(body)
2275+
ioWriter, err := writer.CreateFormFile("file", "test.txt")
2276+
require.NoError(t, err)
2277+
_, err = ioWriter.Write([]byte(strings.Repeat("a", 256)))
2278+
require.NoError(t, err)
2279+
require.NoError(t, writer.Close())
2280+
2281+
c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
2282+
c.Request().SetBody(body.Bytes())
2283+
2284+
_, err = c.FormFile("file")
2285+
require.ErrorIs(t, err, fasthttp.ErrBodyTooLarge)
2286+
}
2287+
2288+
func Test_Ctx_FormValue_BodyLimitExceeded(t *testing.T) {
2289+
t.Parallel()
2290+
2291+
app := New(Config{
2292+
BodyLimit: 64,
2293+
})
2294+
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
2295+
2296+
body := &bytes.Buffer{}
2297+
writer := multipart.NewWriter(body)
2298+
require.NoError(t, writer.WriteField("name", strings.Repeat("a", 256)))
2299+
require.NoError(t, writer.Close())
2300+
2301+
c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
2302+
c.Request().SetBody(body.Bytes())
2303+
2304+
// FormValue should return empty string (default) when the body limit is exceeded.
2305+
val := c.FormValue("name")
2306+
require.Empty(t, val)
2307+
}
2308+
2309+
// Test_Ctx_FormValue_QueryArgs covers the path where the key is found in QueryArgs
2310+
// for a multipart request, which is checked before the multipart form is parsed.
2311+
func Test_Ctx_FormValue_QueryArgs(t *testing.T) {
2312+
t.Parallel()
2313+
2314+
app := New()
2315+
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
2316+
2317+
body := &bytes.Buffer{}
2318+
writer := multipart.NewWriter(body)
2319+
require.NoError(t, writer.WriteField("other", "value"))
2320+
require.NoError(t, writer.Close())
2321+
2322+
c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
2323+
c.Request().SetBody(body.Bytes())
2324+
// Set the key in QueryArgs so the QueryArgs branch returns it.
2325+
c.Request().URI().QueryArgs().Set("name", "alice")
2326+
2327+
require.Equal(t, "alice", c.FormValue("name"))
2328+
}
2329+
2330+
// Test_Ctx_FormValue_PostArgs covers the path where the key is found in PostArgs
2331+
// for a multipart request, which is checked before the multipart form is parsed.
2332+
func Test_Ctx_FormValue_PostArgs(t *testing.T) {
2333+
t.Parallel()
2334+
2335+
app := New()
2336+
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
2337+
2338+
body := &bytes.Buffer{}
2339+
writer := multipart.NewWriter(body)
2340+
require.NoError(t, writer.WriteField("other", "value"))
2341+
require.NoError(t, writer.Close())
2342+
2343+
c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
2344+
c.Request().SetBody(body.Bytes())
2345+
// Manually set a PostArg so that the PostArgs branch returns it.
2346+
c.Request().PostArgs().Set("name", "bob")
2347+
2348+
require.Equal(t, "bob", c.FormValue("name"))
2349+
}
2350+
2351+
// Test_Ctx_FormValue_MultipartKeyNotFound covers the path where the key is absent
2352+
// from the multipart form: returns "" without a default and the provided default otherwise.
2353+
func Test_Ctx_FormValue_MultipartKeyNotFound(t *testing.T) {
2354+
t.Parallel()
2355+
2356+
app := New()
2357+
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
2358+
2359+
body := &bytes.Buffer{}
2360+
writer := multipart.NewWriter(body)
2361+
require.NoError(t, writer.WriteField("other", "value"))
2362+
require.NoError(t, writer.Close())
2363+
2364+
c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
2365+
c.Request().SetBody(body.Bytes())
2366+
2367+
// Key not present → empty string.
2368+
require.Empty(t, c.FormValue("missing"))
2369+
// Key not present + default → default value.
2370+
require.Equal(t, "fallback", c.FormValue("missing", "fallback"))
2371+
}
2372+
2373+
// Test_Ctx_FormValue_NonMultipart covers the non-multipart branch (e.g. URL-encoded body).
2374+
func Test_Ctx_FormValue_NonMultipart(t *testing.T) {
2375+
t.Parallel()
2376+
2377+
app := New()
2378+
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
2379+
2380+
c.Request().Header.Set(HeaderContentType, MIMEApplicationForm)
2381+
c.Request().SetBodyString("name=carol")
2382+
2383+
require.Equal(t, "carol", c.FormValue("name"))
2384+
// Key not present + default → default value.
2385+
require.Equal(t, "fallback", c.FormValue("missing", "fallback"))
2386+
}
2387+
22402388
func Benchmark_Ctx_Fresh_StaleEtag(b *testing.B) {
22412389
app := New()
22422390
c := app.AcquireCtx(&fasthttp.RequestCtx{})
@@ -3930,6 +4078,34 @@ func Test_Ctx_MultipartForm(t *testing.T) {
39304078
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
39314079
}
39324080

4081+
func Test_Ctx_MultipartForm_BodyLimitExceeded(t *testing.T) {
4082+
t.Parallel()
4083+
4084+
app := New(Config{
4085+
BodyLimit: 64,
4086+
})
4087+
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
4088+
4089+
multipartBody := &bytes.Buffer{}
4090+
writer := multipart.NewWriter(multipartBody)
4091+
require.NoError(t, writer.WriteField("name", strings.Repeat("a", 1024)))
4092+
require.NoError(t, writer.Close())
4093+
4094+
var compressed bytes.Buffer
4095+
gz := gzip.NewWriter(&compressed)
4096+
_, err := gz.Write(multipartBody.Bytes())
4097+
require.NoError(t, err)
4098+
require.NoError(t, gz.Flush())
4099+
require.NoError(t, gz.Close())
4100+
4101+
c.Request().Header.Set(HeaderContentType, writer.FormDataContentType())
4102+
c.Request().Header.Set(HeaderContentEncoding, StrGzip)
4103+
c.Request().SetBody(compressed.Bytes())
4104+
4105+
_, err = c.MultipartForm()
4106+
require.ErrorIs(t, err, fasthttp.ErrBodyTooLarge)
4107+
}
4108+
39334109
// go test -v -run=^$ -bench=Benchmark_Ctx_MultipartForm -benchmem -count=4
39344110
func Benchmark_Ctx_MultipartForm(b *testing.B) {
39354111
app := New()

docs/api/ctx.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,7 @@ app.Get("/", func(c fiber.Ctx) error {
869869

870870
### Body
871871

872-
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`.
872+
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).
873873

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

13691369
### MultipartForm
13701370

1371-
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.
1371+
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).
13721372

13731373
```go title="Signature"
13741374
func (c fiber.Ctx) MultipartForm() (*multipart.Form, error)

docs/api/fiber.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ app := fiber.New(fiber.Config{
4545
| Property | Type | Description | Default |
4646
|---------------------------------------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
4747
| <Reference id="appname">AppName</Reference> | `string` | Sets the application name used in logs and the Server header | `""` |
48-
| <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` |
48+
| <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` |
4949
| <Reference id="casesensitive">CaseSensitive</Reference> | `bool` | When enabled, `/Foo` and `/foo` are different routes. When disabled, `/Foo` and `/foo` are treated the same. | `false` |
5050
| <Reference id="cbordecoder">CBORDecoder</Reference> | `utils.CBORUnmarshal` | Allowing for flexibility in using another cbor library for decoding. | `binder.UnimplementedCborUnmarshal` |
5151
| <Reference id="cborencoder">CBOREncoder</Reference> | `utils.CBORMarshal` | Allowing for flexibility in using another cbor library for encoding. | `binder.UnimplementedCborMarshal` |

docs/middleware/compress.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Bodies smaller than 200 bytes remain uncompressed because compression would like
1515
- 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`.
1616
- `HEAD` requests negotiate compression so `Content-Encoding`, `Content-Length`, `ETag`, and `Vary` reflect the encoded representation, but the body is removed before sending.
1717
- 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.
18+
- 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.
1819

1920
## Signatures
2021

0 commit comments

Comments
 (0)