Skip to content

Commit 44aa652

Browse files
committed
add MaxBodyBytes to EndpointMeta for enforcing req payload size
1 parent 58b9082 commit 44aa652

3 files changed

Lines changed: 60 additions & 3 deletions

File tree

apiendpoint/api_endpoint.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"io"
1212
"log/slog"
1313
"net/http"
14+
"strings"
1415
"time"
1516

1617
"github.com/jackc/pgerrcode"
@@ -68,6 +69,11 @@ type EndpointExecuteInterface[TReq any, TResp any] interface {
6869

6970
// EndpointMeta is metadata about an API endpoint.
7071
type EndpointMeta struct {
72+
// MaxBodyBytes is the maximum number of bytes that can be read from the
73+
// request body. If the request body exceeds this number, a 413 error will
74+
// be returned.
75+
MaxBodyBytes int64
76+
7177
// Pattern is the API endpoint's HTTP method and path where it should be
7278
// mounted, which is passed to http.ServeMux by Mount. It should start with
7379
// a verb like `GET` or `POST`, and may contain Go 1.22 path variables like
@@ -136,8 +142,15 @@ func executeAPIEndpoint[TReq any, TResp any](w http.ResponseWriter, r *http.Requ
136142
err := func() error {
137143
var req TReq
138144
if r.Method != http.MethodGet {
145+
if meta.MaxBodyBytes > 0 {
146+
r.Body = http.MaxBytesReader(w, r.Body, meta.MaxBodyBytes)
147+
}
148+
139149
reqData, err := io.ReadAll(r.Body)
140150
if err != nil {
151+
if strings.Contains(err.Error(), "request body too large") {
152+
return apierror.NewRequestEntityTooLarge("Request entity too large")
153+
}
141154
return fmt.Errorf("error reading request body: %w", err)
142155
}
143156

apiendpoint/api_endpoint_test.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"log/slog"
910
"net/http"
1011
"net/http/httptest"
1112
"testing"
@@ -25,6 +26,7 @@ func TestMountAndServe(t *testing.T) {
2526
ctx := context.Background()
2627

2728
type testBundle struct {
29+
logger *slog.Logger
2830
recorder *httptest.ResponseRecorder
2931
}
3032

@@ -41,6 +43,7 @@ func TestMountAndServe(t *testing.T) {
4143
Mount(mux, &postEndpoint{}, opts)
4244

4345
return mux, &testBundle{
46+
logger: logger,
4447
recorder: httptest.NewRecorder(),
4548
}
4649
}
@@ -68,6 +71,28 @@ func TestMountAndServe(t *testing.T) {
6871
requireStatusAndJSONResponse(t, http.StatusOK, &postResponse{Message: "Hello."}, bundle.recorder)
6972
})
7073

74+
t.Run("MaxBodyBytes", func(t *testing.T) {
75+
t.Parallel()
76+
77+
_, bundle := setup(t)
78+
79+
payload := mustMarshalJSON(t, &postRequest{Message: "Hello."})
80+
81+
mux := http.NewServeMux()
82+
endpoint := &postEndpoint{MaxBodyBytes: int64(len(payload))}
83+
Mount(mux, endpoint, &MountOpts{Logger: bundle.logger})
84+
85+
req := httptest.NewRequest(http.MethodPost, "/api/post-endpoint", bytes.NewBuffer(payload))
86+
mux.ServeHTTP(bundle.recorder, req)
87+
requireStatusAndResponse(t, http.StatusCreated, `{"message":"Hello."}`, bundle.recorder)
88+
89+
bundle.recorder = httptest.NewRecorder()
90+
payload = mustMarshalJSON(t, &postRequest{Message: "Hello!!"}) // one longer than previous payload
91+
req = httptest.NewRequest(http.MethodPost, "/api/post-endpoint", bytes.NewBuffer(payload))
92+
mux.ServeHTTP(bundle.recorder, req)
93+
requireStatusAndJSONResponse(t, http.StatusRequestEntityTooLarge, &apierror.APIError{Message: "Request entity too large"}, bundle.recorder)
94+
})
95+
7196
t.Run("MethodNotAllowed", func(t *testing.T) {
7297
t.Parallel()
7398

@@ -275,12 +300,14 @@ func (a *getEndpoint) Execute(_ context.Context, req *getRequest) (*getResponse,
275300

276301
type postEndpoint struct {
277302
Endpoint[postRequest, postResponse]
303+
MaxBodyBytes int64
278304
}
279305

280-
func (*postEndpoint) Meta() *EndpointMeta {
306+
func (a *postEndpoint) Meta() *EndpointMeta {
281307
return &EndpointMeta{
282-
Pattern: "POST /api/post-endpoint",
283-
StatusCode: http.StatusCreated,
308+
MaxBodyBytes: a.MaxBodyBytes,
309+
Pattern: "POST /api/post-endpoint",
310+
StatusCode: http.StatusCreated,
284311
}
285312
}
286313

apierror/api_error.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,23 @@ func NewNotFoundf(format string, a ...any) *NotFound {
131131
return NewNotFound(fmt.Sprintf(format, a...))
132132
}
133133

134+
//
135+
// RequestEntityTooLarge
136+
//
137+
138+
type RequestEntityTooLarge struct { //nolint:errname
139+
APIError
140+
}
141+
142+
func NewRequestEntityTooLarge(message string) *RequestEntityTooLarge {
143+
return &RequestEntityTooLarge{
144+
APIError: APIError{
145+
Message: message,
146+
StatusCode: http.StatusRequestEntityTooLarge,
147+
},
148+
}
149+
}
150+
134151
//
135152
// ServiceUnavailable
136153
//

0 commit comments

Comments
 (0)