mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-09-08 15:41:18 +02:00
refactored body limit middleware to prevent unnecessery reads if already beyound the limit
This commit is contained in:
+3
-1
@@ -3,12 +3,14 @@
|
|||||||
- Minor UI improvements (updated dark primary btn contrast).
|
- Minor UI improvements (updated dark primary btn contrast).
|
||||||
|
|
||||||
- Write the status header for JSON responses only if the fields picker succeed or has acceptable fallback.
|
- Write the status header for JSON responses only if the fields picker succeed or has acceptable fallback.
|
||||||
_This is to allow custom response status code in reaction to failed json writes._
|
_This is to allow custom response status code for failed json writes._
|
||||||
|
|
||||||
- Fixed collection index validator to allow expressions with parenthesis in the optional `WHERE` clause.
|
- Fixed collection index validator to allow expressions with parenthesis in the optional `WHERE` clause.
|
||||||
|
|
||||||
- Clamped arccosine to [-1,1] in the Harvesine formula for the `geoDistance()` filter function to workaround edge case related to float rounding errors for some coordinates.
|
- Clamped arccosine to [-1,1] in the Harvesine formula for the `geoDistance()` filter function to workaround edge case related to float rounding errors for some coordinates.
|
||||||
|
|
||||||
|
- Prevent unnecessery body chunk read if we already known that we are beyond the allowed limit.
|
||||||
|
|
||||||
- Bumped `golang.org/x/*` dependencies to silence security scanners ([#7829](https://github.com/pocketbase/pocketbase/discussions/7829)).
|
- Bumped `golang.org/x/*` dependencies to silence security scanners ([#7829](https://github.com/pocketbase/pocketbase/discussions/7829)).
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -84,32 +84,39 @@ func applyBodyLimit(e *core.RequestEvent, limitBytes int64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// replace the request body
|
// replace the request body
|
||||||
//
|
e.Request.Body = newLimitedReader(e.Request.Body, limitBytes)
|
||||||
// note: we don't use sync.Pool since the size of the elements could vary too much
|
|
||||||
// and it might not be efficient (see https://github.com/golang/go/issues/23199)
|
|
||||||
e.Request.Body = &limitedReader{ReadCloser: e.Request.Body, limit: limitBytes}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newLimitedReader(body io.ReadCloser, limitBytes int64) *limitedReader {
|
||||||
|
return &limitedReader{
|
||||||
|
ReadCloser: body,
|
||||||
|
limit: limitBytes,
|
||||||
|
remaining: limitBytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type limitedReader struct {
|
type limitedReader struct {
|
||||||
io.ReadCloser
|
io.ReadCloser
|
||||||
limit int64
|
limit int64
|
||||||
totalRead int64
|
remaining int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *limitedReader) Read(b []byte) (int, error) {
|
func (r *limitedReader) Read(b []byte) (int, error) {
|
||||||
|
if r.remaining <= 0 {
|
||||||
|
return 0, ErrRequestEntityTooLarge
|
||||||
|
}
|
||||||
|
|
||||||
|
if int64(len(b)) > r.remaining {
|
||||||
|
b = b[0:r.remaining]
|
||||||
|
}
|
||||||
|
|
||||||
n, err := r.ReadCloser.Read(b)
|
n, err := r.ReadCloser.Read(b)
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.totalRead += int64(n)
|
r.remaining -= int64(n)
|
||||||
if r.totalRead > r.limit {
|
|
||||||
return n, ErrRequestEntityTooLarge
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, nil
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// explicit casts to ensure that the main struct methods will be invoked
|
// explicit casts to ensure that the main struct methods will be invoked
|
||||||
@@ -120,6 +127,7 @@ func (r *limitedReader) Reread() {
|
|||||||
rereader, ok := r.ReadCloser.(router.Rereader)
|
rereader, ok := r.ReadCloser.(router.Rereader)
|
||||||
if ok {
|
if ok {
|
||||||
rereader.Reread()
|
rereader.Reread()
|
||||||
|
r.remaining = r.limit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package apis_test
|
package apis_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"net/http"
|
||||||
"fmt"
|
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase/apis"
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
@@ -19,13 +19,26 @@ func TestBodyLimitMiddleware(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
pbRouter.POST("/a", func(e *core.RequestEvent) error {
|
|
||||||
return e.String(200, "a")
|
|
||||||
}) // default global BodyLimit check
|
|
||||||
|
|
||||||
pbRouter.POST("/b", func(e *core.RequestEvent) error {
|
testHandler := func(e *core.RequestEvent) error {
|
||||||
return e.String(200, "b")
|
// read the body multiple times to ensure that the limited
|
||||||
}).Bind(apis.BodyLimit(20))
|
// reader guards and rereads are invoked
|
||||||
|
var result any
|
||||||
|
if err := e.BindBody(&result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.BindBody(&result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.JSON(200, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
const customLimit = 20
|
||||||
|
|
||||||
|
pbRouter.POST("/a", testHandler) // default global BodyLimit check
|
||||||
|
pbRouter.POST("/b", testHandler).Bind(apis.BodyLimit(customLimit))
|
||||||
|
|
||||||
mux, err := pbRouter.BuildMux()
|
mux, err := pbRouter.BuildMux()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,20 +46,82 @@ func TestBodyLimitMiddleware(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
url string
|
name string
|
||||||
size int64
|
url string
|
||||||
expectedStatus int
|
body string
|
||||||
|
lazyContentLength bool
|
||||||
|
expectedStatus int
|
||||||
}{
|
}{
|
||||||
{"/a", 21, 200},
|
{
|
||||||
{"/a", apis.DefaultMaxBodySize + 1, 413},
|
"(eager content-length check) with body = default limit",
|
||||||
{"/b", 20, 200},
|
"/a",
|
||||||
{"/b", 21, 413},
|
`"` + strings.Repeat("a", int(apis.DefaultMaxBodySize-2)) + `"`,
|
||||||
|
false,
|
||||||
|
http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(eager content-length check) with body > default limit",
|
||||||
|
"/a",
|
||||||
|
`"` + strings.Repeat("a", int(apis.DefaultMaxBodySize)) + `"`,
|
||||||
|
false,
|
||||||
|
http.StatusRequestEntityTooLarge,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(lazy content-length check) with body = default limit",
|
||||||
|
"/a",
|
||||||
|
`"` + strings.Repeat("a", int(apis.DefaultMaxBodySize-2)) + `"`,
|
||||||
|
true,
|
||||||
|
http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(lazy content-length check) with body > default limit",
|
||||||
|
"/a",
|
||||||
|
`"` + strings.Repeat("a", int(apis.DefaultMaxBodySize)) + `"`,
|
||||||
|
true,
|
||||||
|
http.StatusRequestEntityTooLarge,
|
||||||
|
},
|
||||||
|
// ---
|
||||||
|
{
|
||||||
|
"(eager content-length check) with body = custom limit",
|
||||||
|
"/b",
|
||||||
|
`"` + strings.Repeat("a", customLimit-2) + `"`,
|
||||||
|
false,
|
||||||
|
http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(eager content-length check) with body > custom limit",
|
||||||
|
"/b",
|
||||||
|
`"` + strings.Repeat("a", customLimit) + `"`,
|
||||||
|
false,
|
||||||
|
http.StatusRequestEntityTooLarge,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(lazy content-length check) with body = custom limit",
|
||||||
|
"/b",
|
||||||
|
`"` + strings.Repeat("a", customLimit-2) + `"`,
|
||||||
|
true,
|
||||||
|
http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(lazy content-length check) with body > custom limit",
|
||||||
|
"/b",
|
||||||
|
`"` + strings.Repeat("a", customLimit) + `"`,
|
||||||
|
true,
|
||||||
|
http.StatusRequestEntityTooLarge,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range scenarios {
|
for _, s := range scenarios {
|
||||||
t.Run(fmt.Sprintf("%s_%d", s.url, s.size), func(t *testing.T) {
|
t.Run(s.name, func(t *testing.T) {
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest("POST", s.url, bytes.NewReader(make([]byte, s.size)))
|
|
||||||
|
req := httptest.NewRequest("POST", s.url, strings.NewReader(s.body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if s.lazyContentLength {
|
||||||
|
req.ContentLength = -1
|
||||||
|
}
|
||||||
|
|
||||||
mux.ServeHTTP(rec, req)
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
result := rec.Result()
|
result := rec.Result()
|
||||||
|
|||||||
Reference in New Issue
Block a user