From f0d5d9a5cb159e18d790973a1e771ae867f9398a Mon Sep 17 00:00:00 2001 From: Erik Dubbelboer Date: Sun, 22 Feb 2026 18:13:40 +0100 Subject: [PATCH] Add WithLimit methods for uncompression (#2147) * Add WithLimit methods for uncompression The current uncompress methods don't enforce a memory limit and are susceptible to things like zip bombs. This pull introduces new methods so retain backwards compatibility. The old methods might be deprecated in the future. * Fix suggestion --- README.md | 1 + brotli.go | 6 +- compress.go | 12 +++- http.go | 192 +++++++++++++++++++++++++++++++++++++++++++-------- http_test.go | 125 +++++++++++++++++++++++++++++++++ server.go | 32 ++++++++- zstd.go | 6 +- 7 files changed, 338 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index e6a9140..02867b6 100644 --- a/README.md +++ b/README.md @@ -379,6 +379,7 @@ Important points: - [r.FormValue()](https://pkg.go.dev/net/http#Request.FormValue) **➜** [ctx.FormValue()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.FormValue) - [r.FormFile()](https://pkg.go.dev/net/http#Request.FormFile) **➜** [ctx.FormFile()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.FormFile) - [r.MultipartForm](https://pkg.go.dev/net/http#Request) **➜** [ctx.MultipartForm()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.MultipartForm) + For untrusted multipart input, use [ctx.MultipartFormWithLimit()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.MultipartFormWithLimit) (or a custom [Server.FormValueFunc](https://pkg.go.dev/github.com/valyala/fasthttp#Server)) to enforce a parsing size limit. - [r.RemoteAddr](https://pkg.go.dev/net/http#Request) **➜** [ctx.RemoteAddr()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.RemoteAddr) - [r.RequestURI](https://pkg.go.dev/net/http#Request) **➜** [ctx.RequestURI()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.RequestURI) - [r.TLS](https://pkg.go.dev/net/http#Request) **➜** [ctx.IsTLS()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.IsTLS) diff --git a/brotli.go b/brotli.go index a89189d..e9cb75a 100644 --- a/brotli.go +++ b/brotli.go @@ -167,12 +167,16 @@ func AppendBrotliBytes(dst, src []byte) []byte { // WriteUnbrotli writes unbrotlied p to w and returns the number of uncompressed // bytes written to w. func WriteUnbrotli(w io.Writer, p []byte) (int, error) { + return writeUnbrotli(w, p, 0) +} + +func writeUnbrotli(w io.Writer, p []byte, maxBodySize int) (int, error) { r := &byteSliceReader{b: p} zr, err := acquireBrotliReader(r) if err != nil { return 0, err } - n, err := copyZeroAlloc(w, zr) + n, err := copyZeroAllocWithLimit(w, zr, maxBodySize) releaseBrotliReader(zr) nn := int(n) if int64(nn) != n { diff --git a/compress.go b/compress.go index c754da4..5507b75 100644 --- a/compress.go +++ b/compress.go @@ -212,12 +212,16 @@ func AppendGzipBytes(dst, src []byte) []byte { // WriteGunzip writes ungzipped p to w and returns the number of uncompressed // bytes written to w. func WriteGunzip(w io.Writer, p []byte) (int, error) { + return writeGunzip(w, p, 0) +} + +func writeGunzip(w io.Writer, p []byte, maxBodySize int) (int, error) { r := &byteSliceReader{b: p} zr, err := acquireGzipReader(r) if err != nil { return 0, err } - n, err := copyZeroAlloc(w, zr) + n, err := copyZeroAllocWithLimit(w, zr, maxBodySize) releaseGzipReader(zr) nn := int(n) if int64(nn) != n { @@ -321,12 +325,16 @@ func AppendDeflateBytes(dst, src []byte) []byte { // WriteInflate writes inflated p to w and returns the number of uncompressed // bytes written to w. func WriteInflate(w io.Writer, p []byte) (int, error) { + return writeInflate(w, p, 0) +} + +func writeInflate(w io.Writer, p []byte, maxBodySize int) (int, error) { r := &byteSliceReader{b: p} zr, err := acquireFlateReader(r) if err != nil { return 0, err } - n, err := copyZeroAlloc(w, zr) + n, err := copyZeroAllocWithLimit(w, zr, maxBodySize) releaseFlateReader(zr) nn := int(n) if int64(nn) != n { diff --git a/http.go b/http.go index 5cd7b74..66cd8da 100644 --- a/http.go +++ b/http.go @@ -492,7 +492,15 @@ var ( // 'Content-Encoding: gzip' for reading un-gzipped body. // Use Body for reading gzipped request body. func (req *Request) BodyGunzip() ([]byte, error) { - return gunzipData(req.Body()) + return req.BodyGunzipWithLimit(0) +} + +// BodyGunzipWithLimit returns un-gzipped body data and limits the size +// of uncompressed body data to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +func (req *Request) BodyGunzipWithLimit(maxBodySize int) ([]byte, error) { + return gunzipData(req.Body(), maxBodySize) } // BodyGunzip returns un-gzipped body data. @@ -501,12 +509,20 @@ func (req *Request) BodyGunzip() ([]byte, error) { // 'Content-Encoding: gzip' for reading un-gzipped body. // Use Body for reading gzipped response body. func (resp *Response) BodyGunzip() ([]byte, error) { - return gunzipData(resp.Body()) + return resp.BodyGunzipWithLimit(0) } -func gunzipData(p []byte) ([]byte, error) { +// BodyGunzipWithLimit returns un-gzipped body data and limits the size +// of uncompressed body data to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +func (resp *Response) BodyGunzipWithLimit(maxBodySize int) ([]byte, error) { + return gunzipData(resp.Body(), maxBodySize) +} + +func gunzipData(p []byte, maxBodySize int) ([]byte, error) { var bb bytebufferpool.ByteBuffer - _, err := WriteGunzip(&bb, p) + _, err := writeGunzip(&bb, p, maxBodySize) if err != nil { return nil, err } @@ -519,7 +535,15 @@ func gunzipData(p []byte) ([]byte, error) { // 'Content-Encoding: br' for reading un-brotlied body. // Use Body for reading brotlied request body. func (req *Request) BodyUnbrotli() ([]byte, error) { - return unBrotliData(req.Body()) + return req.BodyUnbrotliWithLimit(0) +} + +// BodyUnbrotliWithLimit returns un-brotlied body data and limits the size +// of uncompressed body data to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +func (req *Request) BodyUnbrotliWithLimit(maxBodySize int) ([]byte, error) { + return unBrotliData(req.Body(), maxBodySize) } // BodyUnbrotli returns un-brotlied body data. @@ -528,12 +552,20 @@ func (req *Request) BodyUnbrotli() ([]byte, error) { // 'Content-Encoding: br' for reading un-brotlied body. // Use Body for reading brotlied response body. func (resp *Response) BodyUnbrotli() ([]byte, error) { - return unBrotliData(resp.Body()) + return resp.BodyUnbrotliWithLimit(0) } -func unBrotliData(p []byte) ([]byte, error) { +// BodyUnbrotliWithLimit returns un-brotlied body data and limits the size +// of uncompressed body data to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +func (resp *Response) BodyUnbrotliWithLimit(maxBodySize int) ([]byte, error) { + return unBrotliData(resp.Body(), maxBodySize) +} + +func unBrotliData(p []byte, maxBodySize int) ([]byte, error) { var bb bytebufferpool.ByteBuffer - _, err := WriteUnbrotli(&bb, p) + _, err := writeUnbrotli(&bb, p, maxBodySize) if err != nil { return nil, err } @@ -546,7 +578,15 @@ func unBrotliData(p []byte) ([]byte, error) { // 'Content-Encoding: deflate' for reading inflated request body. // Use Body for reading deflated request body. func (req *Request) BodyInflate() ([]byte, error) { - return inflateData(req.Body()) + return req.BodyInflateWithLimit(0) +} + +// BodyInflateWithLimit returns inflated body data and limits the size +// of uncompressed body data to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +func (req *Request) BodyInflateWithLimit(maxBodySize int) ([]byte, error) { + return inflateData(req.Body(), maxBodySize) } // BodyInflate returns inflated body data. @@ -555,7 +595,15 @@ func (req *Request) BodyInflate() ([]byte, error) { // 'Content-Encoding: deflate' for reading inflated response body. // Use Body for reading deflated response body. func (resp *Response) BodyInflate() ([]byte, error) { - return inflateData(resp.Body()) + return resp.BodyInflateWithLimit(0) +} + +// BodyInflateWithLimit returns inflated body data and limits the size +// of uncompressed body data to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +func (resp *Response) BodyInflateWithLimit(maxBodySize int) ([]byte, error) { + return inflateData(resp.Body(), maxBodySize) } func (ctx *RequestCtx) RequestBodyStream() io.Reader { @@ -563,25 +611,41 @@ func (ctx *RequestCtx) RequestBodyStream() io.Reader { } func (req *Request) BodyUnzstd() ([]byte, error) { - return unzstdData(req.Body()) + return req.BodyUnzstdWithLimit(0) +} + +// BodyUnzstdWithLimit returns un-zstd body data and limits the size +// of uncompressed body data to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +func (req *Request) BodyUnzstdWithLimit(maxBodySize int) ([]byte, error) { + return unzstdData(req.Body(), maxBodySize) } func (resp *Response) BodyUnzstd() ([]byte, error) { - return unzstdData(resp.Body()) + return resp.BodyUnzstdWithLimit(0) } -func unzstdData(p []byte) ([]byte, error) { +// BodyUnzstdWithLimit returns un-zstd body data and limits the size +// of uncompressed body data to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +func (resp *Response) BodyUnzstdWithLimit(maxBodySize int) ([]byte, error) { + return unzstdData(resp.Body(), maxBodySize) +} + +func unzstdData(p []byte, maxBodySize int) ([]byte, error) { var bb bytebufferpool.ByteBuffer - _, err := WriteUnzstd(&bb, p) + _, err := writeUnzstd(&bb, p, maxBodySize) if err != nil { return nil, err } return bb.B, nil } -func inflateData(p []byte) ([]byte, error) { +func inflateData(p []byte, maxBodySize int) ([]byte, error) { var bb bytebufferpool.ByteBuffer - _, err := WriteInflate(&bb, p) + _, err := writeInflate(&bb, p, maxBodySize) if err != nil { return nil, err } @@ -590,45 +654,63 @@ func inflateData(p []byte) ([]byte, error) { var ErrContentEncodingUnsupported = errors.New("unsupported Content-Encoding") -// BodyUncompressed returns body data and if needed decompress it from gzip, deflate or Brotli. +// BodyUncompressed returns body data and if needed decompresses it from gzip, +// deflate, brotli or zstd. // // This method may be used if the response header contains // 'Content-Encoding' for reading uncompressed request body. // Use Body for reading the raw request body. func (req *Request) BodyUncompressed() ([]byte, error) { + return req.BodyUncompressedWithLimit(0) +} + +// BodyUncompressedWithLimit returns body data and if needed decompresses it from gzip, +// deflate, brotli or zstd. The size of uncompressed data is limited to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +func (req *Request) BodyUncompressedWithLimit(maxBodySize int) ([]byte, error) { switch string(req.Header.ContentEncoding()) { case "": return req.Body(), nil case "deflate": - return req.BodyInflate() + return req.BodyInflateWithLimit(maxBodySize) case "gzip": - return req.BodyGunzip() + return req.BodyGunzipWithLimit(maxBodySize) case "br": - return req.BodyUnbrotli() + return req.BodyUnbrotliWithLimit(maxBodySize) case "zstd": - return req.BodyUnzstd() + return req.BodyUnzstdWithLimit(maxBodySize) default: return nil, ErrContentEncodingUnsupported } } -// BodyUncompressed returns body data and if needed decompress it from gzip, deflate or Brotli. +// BodyUncompressed returns body data and if needed decompresses it from gzip, +// deflate, brotli or zstd. // // This method may be used if the response header contains // 'Content-Encoding' for reading uncompressed response body. // Use Body for reading the raw response body. func (resp *Response) BodyUncompressed() ([]byte, error) { + return resp.BodyUncompressedWithLimit(0) +} + +// BodyUncompressedWithLimit returns body data and if needed decompresses it from gzip, +// deflate, brotli or zstd. The size of uncompressed data is limited to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +func (resp *Response) BodyUncompressedWithLimit(maxBodySize int) ([]byte, error) { switch string(resp.Header.ContentEncoding()) { case "": return resp.Body(), nil case "deflate": - return resp.BodyInflate() + return resp.BodyInflateWithLimit(maxBodySize) case "gzip": - return resp.BodyGunzip() + return resp.BodyGunzipWithLimit(maxBodySize) case "br": - return resp.BodyUnbrotli() + return resp.BodyUnbrotliWithLimit(maxBodySize) case "zstd": - return resp.BodyUnzstd() + return resp.BodyUnzstdWithLimit(maxBodySize) default: return nil, ErrContentEncodingUnsupported } @@ -1009,9 +1091,26 @@ var ErrNoMultipartForm = errors.New("request Content-Type has bad boundary or is // Returns ErrNoMultipartForm if request's Content-Type // isn't 'multipart/form-data'. // +// This method is equivalent to MultipartFormWithLimit(0), i.e. no body size +// limit is applied during multipart parsing. +// // RemoveMultipartFormFiles must be called after returned multipart form // is processed. func (req *Request) MultipartForm() (*multipart.Form, error) { + return req.MultipartFormWithLimit(0) +} + +// MultipartFormWithLimit returns request's multipart form and limits the +// read multipart body size to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +// +// Returns ErrNoMultipartForm if request's Content-Type +// isn't 'multipart/form-data'. +// +// RemoveMultipartFormFiles must be called after returned multipart form +// is processed. +func (req *Request) MultipartFormWithLimit(maxBodySize int) (*multipart.Form, error) { if req.multipartForm != nil { return req.multipartForm, nil } @@ -1026,30 +1125,46 @@ func (req *Request) MultipartForm() (*multipart.Form, error) { if req.bodyStream != nil { bodyStream := req.bodyStream + var lr *io.LimitedReader if bytes.Equal(ce, strGzip) { - // Do not care about memory usage here. if bodyStream, err = gzip.NewReader(bodyStream); err != nil { return nil, fmt.Errorf("cannot gunzip request body: %w", err) } } else if len(ce) > 0 { return nil, fmt.Errorf("unsupported Content-Encoding: %q", ce) } + if maxBodySize > 0 { + lr = &io.LimitedReader{ + R: bodyStream, + N: int64(maxBodySize) + 1, + } + bodyStream = lr + } mr := multipart.NewReader(bodyStream, req.multipartFormBoundary) req.multipartForm, err = mr.ReadForm(8 * 1024) if err != nil { + if lr != nil && lr.N <= 0 { + return nil, fmt.Errorf("cannot read multipart/form-data body: %w", ErrBodyTooLarge) + } return nil, fmt.Errorf("cannot read multipart/form-data body: %w", err) } + if lr != nil && lr.N <= 0 { + req.RemoveMultipartFormFiles() + return nil, fmt.Errorf("cannot read multipart/form-data body: %w", ErrBodyTooLarge) + } } else { body := req.bodyBytes() if bytes.Equal(ce, strGzip) { - // Do not care about memory usage here. - if body, err = AppendGunzipBytes(nil, body); err != nil { + if body, err = gunzipData(body, maxBodySize); err != nil { return nil, fmt.Errorf("cannot gunzip request body: %w", err) } } else if len(ce) > 0 { return nil, fmt.Errorf("unsupported Content-Encoding: %q", ce) } + if maxBodySize > 0 && len(body) > maxBodySize { + return nil, fmt.Errorf("cannot read multipart/form-data body: %w", ErrBodyTooLarge) + } req.multipartForm, err = readMultipartForm(bytes.NewReader(body), req.multipartFormBoundary, len(body), len(body)) if err != nil { @@ -2464,6 +2579,25 @@ func writeChunk(w *bufio.Writer, b []byte) error { // the given limit. var ErrBodyTooLarge = errors.New("body size exceeds the given limit") +func copyZeroAllocWithLimit(w io.Writer, r io.Reader, maxBodySize int) (int64, error) { + if maxBodySize <= 0 { + return copyZeroAlloc(w, r) + } + + lr := &io.LimitedReader{ + R: r, + N: int64(maxBodySize) + 1, + } + n, err := copyZeroAlloc(w, lr) + if err != nil { + return n, err + } + if lr.N <= 0 { + return n, ErrBodyTooLarge + } + return n, nil +} + func readBody(r *bufio.Reader, contentLength, maxBodySize int, dst []byte) ([]byte, error) { if maxBodySize > 0 && contentLength > maxBodySize { return dst, ErrBodyTooLarge diff --git a/http_test.go b/http_test.go index 5210894..677cf93 100644 --- a/http_test.go +++ b/http_test.go @@ -448,6 +448,131 @@ func TestResponseBodyUncompressed(t *testing.T) { } } +func TestBodyDecodeWithLimitTooLarge(t *testing.T) { + t.Parallel() + + body := bytes.Repeat([]byte("a"), 2*1024) + maxBodySize := 1024 + + testCases := []struct { + name string + encoding string + encode func([]byte) []byte + }{ + { + name: "gzip", + encoding: "gzip", + encode: func(src []byte) []byte { + return AppendGzipBytes(nil, src) + }, + }, + { + name: "deflate", + encoding: "deflate", + encode: func(src []byte) []byte { + return AppendDeflateBytes(nil, src) + }, + }, + { + name: "brotli", + encoding: "br", + encode: func(src []byte) []byte { + return AppendBrotliBytes(nil, src) + }, + }, + { + name: "zstd", + encoding: "zstd", + encode: func(src []byte) []byte { + return AppendZstdBytes(nil, src) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name+"_request_uncompressed", func(t *testing.T) { + var req Request + req.Header.SetContentEncoding(testCase.encoding) + req.SetBodyRaw(testCase.encode(body)) + _, err := req.BodyUncompressedWithLimit(maxBodySize) + if !errors.Is(err, ErrBodyTooLarge) { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run(testCase.name+"_response_uncompressed", func(t *testing.T) { + var resp Response + resp.Header.SetContentEncoding(testCase.encoding) + resp.SetBodyRaw(testCase.encode(body)) + _, err := resp.BodyUncompressedWithLimit(maxBodySize) + if !errors.Is(err, ErrBodyTooLarge) { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestRequestMultipartFormWithLimitGzip(t *testing.T) { + t.Parallel() + + var formBodyBuffer bytes.Buffer + mw := multipart.NewWriter(&formBodyBuffer) + if err := mw.WriteField("foo", strings.Repeat("a", 8*1024)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + boundary := mw.Boundary() + if err := mw.Close(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + formBody := formBodyBuffer.Bytes() + gzippedBody := AppendGzipBytes(nil, formBody) + + t.Run("buffered_too_large", func(t *testing.T) { + var req Request + req.Header.SetMultipartFormBoundary(boundary) + req.Header.SetContentEncoding("gzip") + req.SetBodyRaw(gzippedBody) + + _, err := req.MultipartFormWithLimit(len(formBody) - 1) + if !errors.Is(err, ErrBodyTooLarge) { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("streamed_too_large", func(t *testing.T) { + var req Request + req.Header.SetMultipartFormBoundary(boundary) + req.Header.SetContentEncoding("gzip") + req.SetBodyStream(bytes.NewReader(gzippedBody), len(gzippedBody)) + + _, err := req.MultipartFormWithLimit(len(formBody) - 1) + if !errors.Is(err, ErrBodyTooLarge) { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("buffered_success", func(t *testing.T) { + var req Request + req.Header.SetMultipartFormBoundary(boundary) + req.Header.SetContentEncoding("gzip") + req.SetBodyRaw(gzippedBody) + + f, err := req.MultipartFormWithLimit(len(formBody)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer req.RemoveMultipartFormFiles() + + vv := f.Value["foo"] + if len(vv) != 1 { + t.Fatalf("unexpected values count: %d", len(vv)) + } + if vv[0] != strings.Repeat("a", 8*1024) { + t.Fatalf("unexpected value length: %d", len(vv[0])) + } + }) +} + func TestResponseSwapBodySerial(t *testing.T) { t.Parallel() diff --git a/server.go b/server.go index afeb72f..aa97f24 100644 --- a/server.go +++ b/server.go @@ -210,10 +210,14 @@ type Server struct { // instead. TLSConfig *tls.Config - // FormValueFunc, which is used by RequestCtx.FormValue and support for customizing - // the behaviour of the RequestCtx.FormValue function. + // FormValueFunc customizes the behavior of RequestCtx.FormValue. // - // NetHttpFormValueFunc gives a FormValueFunc func implementation that is consistent with net/http. + // For multipart requests, the default FormValue path calls MultipartForm() + // without a body size limit. If you need a limit for multipart parsing, + // provide a custom FormValueFunc and call MultipartFormWithLimit() there. + // + // NetHttpFormValueFunc gives a FormValueFunc implementation that is + // consistent with net/http. FormValueFunc FormValueFunc nextProtos map[string]ServeHandler @@ -1077,6 +1081,9 @@ func (ctx *RequestCtx) PostArgs() *Args { // Returns ErrNoMultipartForm if request's content-type // isn't 'multipart/form-data'. // +// This method is equivalent to MultipartFormWithLimit(0), i.e. no body size +// limit is applied during multipart parsing. +// // All uploaded temporary files are automatically deleted after // returning from RequestHandler. Either move or copy uploaded files // into new place if you want retaining them. @@ -1090,6 +1097,17 @@ func (ctx *RequestCtx) MultipartForm() (*multipart.Form, error) { return ctx.Request.MultipartForm() } +// MultipartFormWithLimit returns request's multipart form and limits the read +// multipart body size to maxBodySize bytes. +// +// If maxBodySize <= 0, then no limit is applied. +// +// Call this method before FormValue/FormFile if you need a limit for +// multipart parsing. +func (ctx *RequestCtx) MultipartFormWithLimit(maxBodySize int) (*multipart.Form, error) { + return ctx.Request.MultipartFormWithLimit(maxBodySize) +} + // FormFile returns uploaded file associated with the given multipart form key. // // The file is automatically deleted after returning from RequestHandler, @@ -1098,6 +1116,9 @@ func (ctx *RequestCtx) MultipartForm() (*multipart.Form, error) { // Use SaveMultipartFile function for permanently saving uploaded file. // // The returned file header is valid until your request handler returns. +// +// For multipart requests with untrusted input, call MultipartFormWithLimit() +// before FormFile. func (ctx *RequestCtx) FormFile(key string) (*multipart.FileHeader, error) { mf, err := ctx.MultipartForm() if err != nil { @@ -1182,6 +1203,10 @@ func SaveMultipartFile(fh *multipart.FileHeader, path string) (err error) { // - FormFile for obtaining uploaded files. // // The returned value is valid until your request handler returns. +// +// For multipart requests with untrusted input, either call +// MultipartFormWithLimit() before FormValue or provide a custom +// Server.FormValueFunc that uses MultipartFormWithLimit(). func (ctx *RequestCtx) FormValue(key string) []byte { if ctx.formValueFunc != nil { return ctx.formValueFunc(ctx, key) @@ -1189,6 +1214,7 @@ func (ctx *RequestCtx) FormValue(key string) []byte { return defaultFormValue(ctx, key) } +// FormValueFunc customizes how RequestCtx.FormValue resolves a value. type FormValueFunc func(*RequestCtx, string) []byte var ( diff --git a/zstd.go b/zstd.go index 169783a..bbf503b 100644 --- a/zstd.go +++ b/zstd.go @@ -139,12 +139,16 @@ func AppendZstdBytes(dst, src []byte) []byte { // WriteUnzstd writes unzstd p to w and returns the number of uncompressed // bytes written to w. func WriteUnzstd(w io.Writer, p []byte) (int, error) { + return writeUnzstd(w, p, 0) +} + +func writeUnzstd(w io.Writer, p []byte, maxBodySize int) (int, error) { r := &byteSliceReader{b: p} zr, err := acquireZstdReader(r) if err != nil { return 0, err } - n, err := copyZeroAlloc(w, zr) + n, err := copyZeroAllocWithLimit(w, zr, maxBodySize) releaseZstdReader(zr) nn := int(n) if int64(nn) != n {