Skip to content

feat(http): add SizeLimitHandler to enforce request body size limit#6658

Open
bladehan1 wants to merge 8 commits intotronprotocol:developfrom
bladehan1:feat/request_size
Open

feat(http): add SizeLimitHandler to enforce request body size limit#6658
bladehan1 wants to merge 8 commits intotronprotocol:developfrom
bladehan1:feat/request_size

Conversation

@bladehan1
Copy link
Copy Markdown
Collaborator

What does this PR do?

Add Jetty SizeLimitHandler at the server handler level to enforce request body size limits for all HTTP and JSON-RPC endpoints. Oversized requests are rejected with HTTP 413 before the body is fully buffered into memory.

  • Introduce node.http.maxMessageSize and node.jsonrpc.maxMessageSize as independent, configurable size limits
  • Default: GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE (4 MB), consistent with gRPC defaults
  • Wire SizeLimitHandler into HttpService.initContextHandler() as the outermost handler
  • Each HttpService subclass (4 HTTP + 3 JSON-RPC) sets maxRequestSize from the corresponding config getter
  • Deprecate Util.checkBodySize() — retained as fallback for backward compatibility

Why are these changes required?

Previously, HTTP request body size was only validated at the application layer (Util.checkBodySize()), which reads the entire body into memory before checking. The JSON-RPC interface had no size validation at all. This allows an attacker to send arbitrarily large payloads, causing OOM and denial of service.

Moving the limit to the Jetty handler chain provides:

  1. Early rejection — oversized requests are stopped before reaching servlet code
  2. Streaming enforcement — limits are enforced during read, not after full buffering
  3. Unified coverage — all HTTP and JSON-RPC endpoints are protected by a single mechanism
  4. Independent tuning — operators can configure HTTP and JSON-RPC limits separately

Closes #6604

This PR has been tested by:

  • Unit Tests (SizeLimitHandlerTest: boundary, independent limits, UTF-8 byte counting)
  • Unit Tests (ArgsTest: default value alignment)

Follow up

  • Remove Util.checkBodySize() callers in a follow-up PR once this is stable
  • Compression bomb (gzip decompression) protection is a separate concern, tracked independently

Extra details

Files changed: 14 (+253 / -2)

Component Changes
HttpService Add maxRequestSize field, wire SizeLimitHandler in initContextHandler()
Args / ConfigKey / CommonParameter Parse node.http.maxMessageSize and node.jsonrpc.maxMessageSize
7 service subclasses Set maxRequestSize from protocol-specific getter in constructor
Util.checkBodySize() Mark @Deprecated
SizeLimitHandlerTest New: 8 tests covering HTTP/JSON-RPC limits, boundary, UTF-8, independence
ArgsTest Align assertions with 4 MB defaults

…imit

Add SizeLimitHandler at the Jetty server level to reject oversized
request bodies before they are fully buffered into memory. This prevents
OOM attacks via arbitrarily large HTTP payloads that bypass the existing
application-level Util.checkBodySize() check (which reads the entire
body first) and the JSON-RPC interface (which had no size validation).
Introduce node.http.maxMessageSize and node.jsonrpc.maxMessageSize to
allow HTTP and JSON-RPC services to enforce separate request body size
limits via Jetty SizeLimitHandler, decoupled from gRPC config.

- Default: 4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE (16 MB)
- Validation: reject <= 0 with TronError(PARAMETER_INIT) at startup
- Each HttpService subclass sets its own maxRequestSize in constructor
- SizeLimitHandlerTest covers independent limits, boundary, UTF-8 bytes
@xxo1shine
Copy link
Copy Markdown
Collaborator

@bladehan1 One observation on the config validation in Args.java: the guard currently rejects <= 0 values with TronError, but there is no upper-bound check. An operator who accidentally sets node.http.maxMessageSize = 2147483647 would silently run with a 2 GB limit. Adding a reasonable maximum (e.g. 128 MB) with an explicit warn-or-throw would make misconfiguration more visible.

@bladehan1
Copy link
Copy Markdown
Collaborator Author

@xxo1shine
Thanks for the review. I think that when users manually configure values, it's unnecessary to set all boundaries except for error-prone values. Especially for values ​​that are obviously likely to have problems, setting boundaries is unnecessary.

}
}

@Deprecated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Marking this helper as deprecated does not make the new HTTP limit effective yet. PostParams.getPostParams() and many servlets still call Util.checkBodySize(), and that method is still enforcing parameter.getMaxMessageSize() (the gRPC limit), not httpMaxMessageSize. So any request whose body is > node.rpc.maxMessageSize but <= node.http.maxMessageSize will pass Jetty and then still be rejected in the servlet layer, which means the new independent HTTP setting is not actually honored for a large part of the API surface.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, thanks. checkBodySize() has been updated to use parameter.getHttpMaxMessageSize() instead of parameter.getMaxMessageSize(), so the servlet-layer fallback now honors the independent HTTP limit. See the latest push.

ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath(this.contextPath);
this.apiServer.setHandler(context);
SizeLimitHandler sizeLimitHandler = new SizeLimitHandler(this.maxRequestSize, -1);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This moves oversized-request handling in front of every servlet, so those requests never reach RateLimiterServlet.service() / Util.processError(). Today the HTTP APIs consistently set application/json and serialize failures through Util.printErrorMsg(...); after this change an over-limit body gets Jetty's default 413 response instead. That is a client-visible behavior change for existing callers, and the new test only checks status codes so it would not catch the response-format regression.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for flagging the response-format difference. I did a detailed comparison:

Before (checkBodySize rejection):

  • HTTP status: 200 OK (processError() does not set status code)
  • Body: {"Error":"java.lang.Exception : body size is too big, the limit is 4194304"}

After (SizeLimitHandler rejection):

  • HTTP status: 413 Payload Too Large
  • Body: Jetty default error page (non-JSON)

The format is indeed not fully compatible, but I believe this is acceptable:

  1. The existing behavior is itself incorrect — returning 200 OK with an error JSON body violates HTTP semantics. Clients that only check status codes would incorrectly treat the oversized request as successful. The new 413 is the proper HTTP response for this scenario.

  2. The trigger threshold is very high — default is 4 MB. Normal API requests are far below this. Only abnormal or malicious payloads hit this limit, so the impact surface is negligible for legitimate clients.

  3. 413 is a standard HTTP status code — all HTTP client libraries handle it correctly. Clients already need to handle non-JSON infrastructure errors (e.g., Jetty 503, proxy 502/504).

  4. The new layer provides a real security benefitSizeLimitHandler rejects during streaming, before the body is fully buffered into memory, which is the core OOM protection. Falling back to application-layer formatting would defeat this purpose.

@halibobo1205 halibobo1205 added this to the GreatVoyage-v4.8.2 milestone Apr 9, 2026
@halibobo1205 halibobo1205 added topic:api rpc/http related issue Improvement labels Apr 9, 2026
checkBodySize() was enforcing maxMessageSize (gRPC limit) instead of
httpMaxMessageSize, causing the independent HTTP size setting to be
ineffective at the servlet layer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bladehan1 bladehan1 force-pushed the feat/request_size branch from 24abc3a to 3048750 Compare April 9, 2026 10:43
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath(this.contextPath);
this.apiServer.setHandler(context);
SizeLimitHandler sizeLimitHandler = new SizeLimitHandler(this.maxRequestSize, -1);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

SizeLimitHandler only guarantees a pre-servlet 413 when Content-Length is known. For chunked / unknown-length requests, enforcement happens while downstream code is reading the request body. In the current codebase many HTTP handlers catch broad Exception around request.getReader(), and RateLimiterServlet also catches unexpected Exception, so the streaming over-limit exception can be absorbed before Jetty turns it into a 413. That means the PR still does not prove the “uniform 413” behavior described in the issue for requests without Content-Length.

Assert.assertEquals(200, post(httpServerUri, new StringEntity("small body")));
}

@Test
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

these tests all use StringEntity, so they exercise the fixed-length / Content-Length path only. They do not cover chunked or unknown-length requests, where SizeLimitHandler enforces during body reads instead of before servlet dispatch. Since the existing servlet chain has broad catch (Exception) blocks, we need at least one end-to-end test for the streaming path against a real HTTP servlet and the JSON-RPC servlet, and it should assert that an oversized request still surfaces as HTTP 413

int defaultHttpMaxMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE;
PARAMETER.httpMaxMessageSize = config.hasPath(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE)
? config.getInt(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) : defaultHttpMaxMessageSize;
if (PARAMETER.httpMaxMessageSize <= 0) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The new config validation rejects non-positive values, but extremely large limits are still accepted silently. I don’t think a hard cap is mandatory, but at least warning on suspiciously large values would make misconfiguration easier to catch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Improvement topic:api rpc/http related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Unified HTTP Request Body Size Limit

5 participants