Skip to content

Conversation

@bgentry
Copy link
Contributor

@bgentry bgentry commented Mar 14, 2025

One more feature building on #4: making it easy to enforce a max request payload size.

If a middleware applies http.MaxBytesReader and we encounter an
*http.MaxBytesError when reading the request body, return an HTTP 413
with a clean error.

This does not actually have a function for applying a limit, only for
handling those errors when they occur.

@bgentry bgentry requested a review from brandur March 14, 2025 22:46
@brandur
Copy link
Contributor

brandur commented Mar 15, 2025

Hm, the only thing I don't love about this is that it'd have to be configured on every endpoint — so you'd end up adding quite a bit of noise to each one, and it'd really easy to accidentally forget.

I've traditionally implemented this with middleware:

type MaxBytesReaderMiddleware struct {
	pservice.BaseMiddleware
	maxBytes int64
}

func NewMaxBytesReaderMiddleware() *MaxBytesReaderMiddleware {
	return &MaxBytesReaderMiddleware{
		// 1 megabyte
		maxBytes: 1 * 1024 * 1024,
	}
}

func (m *MaxBytesReaderMiddleware) Wrapper(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Body != nil {
			r.Body = http.MaxBytesReader(w, r.Body, m.maxBytes)
		}

		next.ServeHTTP(w, r)
	})
}

This works, but doesn't produce the nice 413.

Looking at this again, I'm kind of wondering if it might not be a bad idea to have something like a "high level" apiframe middleware variant that's able to intercept a request at the top of the stack, modify it (i.e. make req.Body a max reader), and then potentially change out an error after response to something better (like a custom API error instead of an internal error). Maybe this would even look like River hooks ... (i.e. functions instead of a wrapper, hmm).

@bgentry
Copy link
Contributor Author

bgentry commented Mar 15, 2025

Yeah, I was also thinking it doesn’t make sense to configure for every endpoint and could probably just be a global safety check. Really you just want to put a reasonable cap on most payload sizes to avoid the DoS vector, and unless you have some endpoints that take large payloads (file uploads & such) you can probably get away with a single limit everywhere.

Do you want to take a crack at the hooks option?

@brandur
Copy link
Contributor

brandur commented Mar 15, 2025

Yeah, I was also thinking it doesn’t make sense to configure for every endpoint and could probably just be a global safety check. Really you just want to put a reasonable cap on most payload sizes to avoid the DoS vector, and unless you have some endpoints that take large payloads (file uploads & such) you can probably get away with a single limit everywhere.

Do you want to take a crack at the hooks option?

What do you think about repurposing this to just put in with a hard-coded don't-DDOS-us-bro limit to the core API infrastructure, then I could go and try to extract it to a separate middleware later. (Or even not, we could probably just get away with the global limit for a while.)

@bgentry bgentry force-pushed the bg-mount-opts branch 2 times, most recently from c249d72 to af2dbc2 Compare March 15, 2025 18:25
Base automatically changed from bg-mount-opts to master March 15, 2025 18:25
@bgentry bgentry force-pushed the bg-max-body-bytes branch from 44aa652 to 12e48ce Compare March 16, 2025 00:30
@bgentry bgentry changed the title add MaxBodyBytes to EndpointMeta for enforcing req payload size cleanly handle http.MaxBytesError for enforcing req payload size Mar 16, 2025
@bgentry
Copy link
Contributor Author

bgentry commented Mar 16, 2025

@brandur had another idea here and reworked it. If a middleware applies http.MaxBytesReader and we encounter an *http.MaxBytesError when reading the request body, return an HTTP 413 with a clean error. This does not actually have a function for applying a limit, only for handling those errors when they occur.

Copy link
Contributor

@brandur brandur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brandur had another idea here and reworked it. If a middleware applies http.MaxBytesReader and we encounter an *http.MaxBytesError when reading the request body, return an HTTP 413 with a clean error. This does not actually have a function for applying a limit, only for handling those errors when they occur.

Cool, but wait, who applies the limit then? A middleware/hook we put in later?

if err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
return apierror.NewRequestEntityTooLarge("Request entity too large")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just keep punctuation as a convention on errors in here (the others have it already).

Suggested change
return apierror.NewRequestEntityTooLarge("Request entity too large")
return apierror.NewRequestEntityTooLarge("Request entity too large.")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@bgentry
Copy link
Contributor Author

bgentry commented Mar 16, 2025

Cool, but wait, who applies the limit then? A middleware/hook we put in later?

Yes, I did this in https://github.com/riverqueue/riverapi/pull/36/files#diff-0eb779b9e49d8e44b0f36923fdb8d87d5ee024f886eefc45deec4ec88380a087R57-R63

Basically this adds some built-in handling as part of apiframe for when users happen to use http.MaxBytesReader, only handling the specific error it produces and only when reading the request body.

I'm not sure if there's a cleaner way to do this globally given the current structure of this package—there's not really a global place where you could even configure it, only individual endpoints/handlers that mount themselves to a mux. But the error specifically shows up when reading the request body, which is something the framework does handle, so it sort of needs to be the thing that detects the specific error.

@bgentry bgentry force-pushed the bg-max-body-bytes branch from 12e48ce to 137df20 Compare March 16, 2025 02:44
If a middleware applies `http.MaxBytesReader` and we encounter an
`*http.MaxBytesError` when reading the request body, return an HTTP 413
with a clean error.
@bgentry bgentry force-pushed the bg-max-body-bytes branch from 137df20 to 245f9dd Compare March 16, 2025 03:20
@bgentry bgentry enabled auto-merge (squash) March 16, 2025 03:20
@bgentry bgentry merged commit 897cf5c into master Mar 16, 2025
2 checks passed
@bgentry bgentry deleted the bg-max-body-bytes branch March 16, 2025 03:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants