Repository navigation
feat(evmrpc): pre-decode request size admission control (PLT-295) #3648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
masih
merged 13 commits into
main
from
amir/plt-295-simulation-limiting-before-json-decode
Jun 30, 2026
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
9c6965a
Added evmrpc request limiter and test
amir-deris 9095da2
Allocate max body request only when content length is unknown/chunked…
amir-deris dc7991f
Merge branch 'main' into amir/plt-295-simulation-limiting-before-json…
amir-deris 137a26e
Configured sei legacy wrapper to use config max body
amir-deris 954c23f
Merge branch 'main' into amir/plt-295-simulation-limiting-before-json…
masih 6a068c4
Fix the incosistency between chunked/unknow-length request with overs…
amir-deris 439a735
Added comment about 3 layers of request size validation
amir-deris fcaa141
Added e2e test
amir-deris 3db4c95
Record oversize rejection metric on legacy gate 413 paths
amir-deris ad1f95c
Updated comment
amir-deris 4867584
Merge branch 'main' into amir/plt-295-simulation-limiting-before-json…
amir-deris f6abb41
Fixed int conversion
amir-deris fcb9009
Updated the requestSizeLimiter comment
amir-deris File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package evmrpc | ||
|
|
||
| import ( | ||
| "net/http" | ||
|
|
||
| "golang.org/x/sync/semaphore" | ||
| ) | ||
|
|
||
| // defaultMaxRequestBodyBytes matches github.com/ethereum/go-ethereum/rpc.defaultBodyLimit, | ||
| // the per-request HTTP body cap the inner JSON-RPC server applies. It is used when | ||
| // max_request_body_bytes is left at 0 ("use the default"). | ||
| const defaultMaxRequestBodyBytes int64 = 5 * 1024 * 1024 | ||
|
|
||
| // requestSizeLimiter is an HTTP middleware that bounds peak decode-time memory by | ||
| // admitting JSON-RPC requests *before* the body is buffered or decoded. It enforces: | ||
| // | ||
| // - maxBody: per-request body cap. Over-Content-Length requests get 413; the body is | ||
| // also wrapped in http.MaxBytesReader so chunked / mis-declared bodies can't exceed it. | ||
| // - budget: a size-weighted global semaphore bounding bytes admitted concurrently; | ||
| // over-budget requests get 429. The reservation is held for the full inner request | ||
| // (not just decode) and trusts the declared Content-Length — conservative by design, | ||
| // so slow or trickled requests can exhaust the budget even when little is buffered. | ||
| // Actual bytes stay hard-capped by MaxBytesReader; per-IP rate limiting will be | ||
| // wired in front of this handler. | ||
| type requestSizeLimiter struct { | ||
| inner http.Handler | ||
| maxBody int64 // always > 0 after construction | ||
| budget *semaphore.Weighted // nil when the global budget is disabled | ||
| } | ||
|
|
||
| // newRequestSizeLimiter wraps inner with pre-decode admission control. maxBody <= 0 | ||
| // normalizes to defaultMaxRequestBodyBytes (the per-request cap is always applied — | ||
| // 0 means "use the default", never "no cap"). maxConcurrentBytes <= 0 disables the | ||
| // global budget. If a positive budget is smaller than maxBody it is raised to maxBody | ||
| // so that a single maximum-size request can always be admitted. | ||
| func newRequestSizeLimiter(inner http.Handler, maxBody, maxConcurrentBytes int64) http.Handler { | ||
| if maxBody <= 0 { | ||
| maxBody = defaultMaxRequestBodyBytes | ||
| } | ||
| l := &requestSizeLimiter{inner: inner, maxBody: maxBody} | ||
| if maxConcurrentBytes > 0 { | ||
| if maxConcurrentBytes < maxBody { | ||
| maxConcurrentBytes = maxBody | ||
| } | ||
| l.budget = semaphore.NewWeighted(maxConcurrentBytes) | ||
| } | ||
| return l | ||
| } | ||
|
|
||
| func (l *requestSizeLimiter) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
| // Per-request cap on the declared length (header-only, before any body read). | ||
| if r.ContentLength > l.maxBody { | ||
| recordRequestRejected(r.Context(), rejectReasonOversize) | ||
| http.Error(w, "request body too large", http.StatusRequestEntityTooLarge) | ||
| return | ||
| } | ||
| // Backstop for chunked / mis-declared bodies: cap the bytes actually readable. | ||
| r.Body = http.MaxBytesReader(w, r.Body, l.maxBody) | ||
|
|
||
| if l.budget != nil { | ||
| // For requests with a known length Go's HTTP server enforces Content-Length, | ||
| // so it is a sound upper bound on bytes read (and 0 means an empty body that | ||
| // reserves nothing). Chunked / unknown-length requests report ContentLength | ||
| // == -1; reserve the worst case (maxBody) for them. weight is therefore always | ||
| // in [0, maxBody], and the budget is >= maxBody, so the request can always fit. | ||
| weight := r.ContentLength | ||
| if weight < 0 { | ||
| weight = l.maxBody | ||
| } | ||
| if !l.budget.TryAcquire(weight) { | ||
| recordRequestRejected(r.Context(), rejectReasonBusy) | ||
| http.Error(w, "server busy", http.StatusTooManyRequests) | ||
| return | ||
| } | ||
| defer l.budget.Release(weight) | ||
| } | ||
|
|
||
| l.inner.ServeHTTP(w, r) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.