Skip to content
1 change: 1 addition & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati

r := newRouter()
r.UseBypass(middleware.RealIP)
r.UseBypass(limitBodySize(defaultMaxBodySize))
// Inject base context values into each request (replaces chi.ServerBaseContext from chi v4)
r.UseBypass(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
Expand Down
19 changes: 19 additions & 0 deletions api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,27 @@ import (

const (
jwsSignatureHeaderName = "x-nf-sign"

// defaultMaxBodySize caps the number of bytes the server will read from a
// request body. It is enforced by limitBodySize and exists to prevent a
// client from holding memory open with a multi-gigabyte upload.
defaultMaxBodySize int64 = 1 << 20 // 1 MiB
)

// limitBodySize wraps each request body in http.MaxBytesReader so that any
// reader (json.NewDecoder, io.ReadAll, etc.) returns an error once the limit is
// reached instead of buffering the entire body in memory.
func limitBodySize(maxSize int64) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Body != nil && r.Body != http.NoBody {
r.Body = http.MaxBytesReader(w, r.Body, maxSize)
}
next.ServeHTTP(w, r)
})
}
}

type FunctionHooks map[string][]string

type NetlifyMicroserviceClaims struct {
Expand Down
65 changes: 65 additions & 0 deletions api/middleware_bodysize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package api

import (
"bytes"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLimitBodySize(t *testing.T) {
const limit int64 = 16

tests := []struct {
name string
body string
wantErr bool
wantRead string
}{
{"under limit reads fully", "small body", false, "small body"},
{"at limit reads fully", strings.Repeat("a", int(limit)), false, strings.Repeat("a", int(limit))},
{"over limit errors", strings.Repeat("a", int(limit)+1), true, ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
readErr error
readBody []byte
)

handler := limitBodySize(limit)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
readBody, readErr = io.ReadAll(r.Body)
}))

req := httptest.NewRequest("POST", "/", bytes.NewBufferString(tt.body))
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)

if tt.wantErr {
require.Error(t, readErr)
var mbe *http.MaxBytesError
assert.True(t, errors.As(readErr, &mbe), "expected *http.MaxBytesError, got %T", readErr)
return
}
require.NoError(t, readErr)
assert.Equal(t, tt.wantRead, string(readBody))
})
}
}

func TestLimitBodySizeNilBodyIsSafe(t *testing.T) {
handler := limitBodySize(1024)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
}
Loading