HTTP Middleware for Request Body Limits

Cap JSON request bodies with MaxBytesReader, reject oversized payloads with 413, and reuse the middleware across handlers.

GoSecurity Boundaries

Code

snippet
package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
)

type Middleware func(http.Handler) http.Handler

type Payload struct {
	Name string `json:"name"`
}

func LimitBody(maxBytes int64) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
			next.ServeHTTP(w, r)
		})
	}
}

func decodePayload(w http.ResponseWriter, r *http.Request) (Payload, error) {
	var payload Payload
	decoder := json.NewDecoder(r.Body)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&payload); err != nil {
		var tooLarge *http.MaxBytesError
		if errors.As(err, &tooLarge) {
			http.Error(w, fmt.Sprintf("body exceeds %d bytes", tooLarge.Limit), http.StatusRequestEntityTooLarge)
			return payload, err
		}
		http.Error(w, "invalid JSON body", http.StatusBadRequest)
		return payload, err
	}
	if strings.TrimSpace(payload.Name) == "" {
		http.Error(w, "name is required", http.StatusUnprocessableEntity)
		return payload, errors.New("name is required")
	}
	return payload, nil
}

func main() {
	handler := LimitBody(64)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		payload, err := decodePayload(w, r)
		if err != nil {
			return
		}
		w.WriteHeader(http.StatusCreated)
		fmt.Fprintf(w, "created %s\n", payload.Name)
	}))

	bodies := []string{
		`{"name":"ada"}`,
		`{"name":"` + strings.Repeat("x", 80) + `"}`,
	}
	for _, body := range bodies {
		request := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
		recorder := httptest.NewRecorder()
		handler.ServeHTTP(recorder, request)
		fmt.Printf("status=%d body=%q\n", recorder.Code, strings.TrimSpace(recorder.Body.String()))
	}
}