From 50496d5a05d21399e2be7907f8f5fc8d9a1587d4 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 19 Mar 2026 15:21:24 +0800 Subject: [PATCH 01/13] feat(http): add Jetty SizeLimitHandler to enforce request body size limit 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). --- .../tron/common/application/HttpService.java | 6 +- .../org/tron/core/services/http/Util.java | 1 + .../common/jetty/SizeLimitHandlerTest.java | 139 ++++++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index e9a902002ba..f3428d37fdb 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.server.ConnectionLimit; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.SizeLimitHandler; import org.eclipse.jetty.servlet.ServletContextHandler; import org.tron.core.config.args.Args; @@ -63,7 +64,10 @@ protected void initServer() { protected ServletContextHandler initContextHandler() { ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); context.setContextPath(this.contextPath); - this.apiServer.setHandler(context); + int maxMessageSize = Args.getInstance().getMaxMessageSize(); + SizeLimitHandler sizeLimitHandler = new SizeLimitHandler(maxMessageSize, -1); + sizeLimitHandler.setHandler(context); + this.apiServer.setHandler(sizeLimitHandler); return context; } diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 2b6b929d8a0..f112e13c818 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -327,6 +327,7 @@ public static Transaction packTransaction(String strTransaction, boolean selfTyp } } + @Deprecated public static void checkBodySize(String body) throws Exception { CommonParameter parameter = Args.getInstance(); if (body.getBytes().length > parameter.getMaxMessageSize()) { diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java new file mode 100644 index 00000000000..d06f86963bf --- /dev/null +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -0,0 +1,139 @@ +package org.tron.common.jetty; + +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.common.TestConstants; +import org.tron.common.application.HttpService; +import org.tron.common.utils.PublicMethod; +import org.tron.core.config.args.Args; + +/** + * Tests the {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size + * enforcement configured in {@link HttpService initContextHandler()}. + * + *

Unlike a standalone Jetty micro-server, this test creates a concrete + * {@link HttpService} subclass and calls {@link HttpService#start()}, so the + * production code paths of {@code initServer()} and {@code initContextHandler()} + * are exercised directly and reflected in code-coverage reports.

+ * + *

Key behaviours proven:

+ * + */ +@Slf4j +public class SizeLimitHandlerTest { + + private static final int MAX_BODY_SIZE = 1024; + + private static TestHttpService httpService; + private static URI serverUri; + private static CloseableHttpClient client; + + /** Echoes the raw request-body bytes back so tests can inspect what arrived. */ + public static class EchoServlet extends HttpServlet { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + byte[] body = ByteStreams.toByteArray(req.getInputStream()); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/octet-stream"); + resp.getOutputStream().write(body); + } + } + + /** Minimal concrete {@link HttpService} that registers only an {@link EchoServlet}. */ + static class TestHttpService extends HttpService { + TestHttpService(int port) { + this.port = port; + this.contextPath = "/"; + } + + @Override + protected void addServlet(ServletContextHandler context) { + context.addServlet(new ServletHolder(new EchoServlet()), "/*"); + } + } + + /** + * Initialises {@link Args} and starts a real {@link HttpService} whose + * {@code initServer()} and {@code initContextHandler()} are the production + * implementations — guaranteeing test coverage of the new + * {@code SizeLimitHandler} wiring. + */ + @BeforeClass + public static void setup() throws Exception { + String dbPath = Files.createTempDirectory("sizelimit-test").toString(); + Args.setParam(new String[]{"--output-directory", dbPath}, TestConstants.TEST_CONF); + Args.getInstance().setMaxMessageSize(MAX_BODY_SIZE); + + int port = PublicMethod.chooseRandomPort(); + httpService = new TestHttpService(port); + httpService.start(); + + serverUri = new URI(String.format("http://localhost:%d/", port)); + client = HttpClients.createDefault(); + } + + @AfterClass + public static void teardown() throws Exception { + try { + if (client != null) { + client.close(); + } + } finally { + if (httpService != null) { + httpService.stop(); + } + Args.clearParam(); + } + } + + // -- body-size tests (covers HttpService.initContextHandler) --------------- + + @Test + public void testBodyWithinLimit() throws Exception { + Assert.assertEquals(200, post(new StringEntity("small body"))); + } + + @Test + public void testBodyExceedsLimit() throws Exception { + Assert.assertEquals(413, post(new StringEntity(repeat('a', MAX_BODY_SIZE + 1)))); + } + + // -- helpers --------------------------------------------------------------- + + /** POSTs with the given entity and returns the HTTP status code. */ + private int post(HttpEntity entity) throws Exception { + HttpPost req = new HttpPost(serverUri); + req.setEntity(entity); + HttpResponse resp = client.execute(req); + EntityUtils.consume(resp.getEntity()); + return resp.getStatusLine().getStatusCode(); + } + + /** Returns a string of {@code n} repetitions of {@code c}. */ + private static String repeat(char c, int n) { + return new String(new char[n]).replace('\0', c); + } +} From 736f2ed272d4e310ac8a269476ebfbbf4dc19c3d Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 19 Mar 2026 15:55:39 +0800 Subject: [PATCH 02/13] feat(http) : wait for HttpService startup future in SizeLimitHandlerTest --- .../test/java/org/tron/common/jetty/SizeLimitHandlerTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index d06f86963bf..2d174f14629 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.net.URI; import java.nio.file.Files; +import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -89,7 +90,7 @@ public static void setup() throws Exception { int port = PublicMethod.chooseRandomPort(); httpService = new TestHttpService(port); - httpService.start(); + httpService.start().get(10, TimeUnit.SECONDS); serverUri = new URI(String.format("http://localhost:%d/", port)); client = HttpClients.createDefault(); From b2ade664573c650df55a574e80c41aa8e2db5f74 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 23 Mar 2026 18:31:57 +0800 Subject: [PATCH 03/13] feat(http): add independent maxMessageSize for HTTP and JSON-RPC 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 --- .../common/parameter/CommonParameter.java | 6 + .../tron/common/application/HttpService.java | 5 +- .../java/org/tron/core/config/args/Args.java | 14 ++ .../org/tron/core/config/args/ConfigKey.java | 2 + .../services/http/FullNodeHttpApiService.java | 1 + .../solidity/SolidityNodeHttpApiService.java | 1 + .../JsonRpcServiceOnPBFT.java | 1 + .../JsonRpcServiceOnSolidity.java | 1 + .../http/PBFT/HttpApiOnPBFTService.java | 1 + .../solidity/HttpApiOnSolidityService.java | 1 + .../jsonrpc/FullNodeJsonRpcHttpService.java | 1 + .../common/jetty/SizeLimitHandlerTest.java | 143 +++++++++++++----- .../org/tron/core/config/args/ArgsTest.java | 3 + 13 files changed, 144 insertions(+), 36 deletions(-) diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index fbb39a13288..abb93765050 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -216,6 +216,12 @@ public class CommonParameter { public int maxMessageSize; @Getter @Setter + public int httpMaxMessageSize; + @Getter + @Setter + public int jsonRpcMaxMessageSize; + @Getter + @Setter public int maxHeaderListSize; @Getter @Setter diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index f3428d37fdb..b7032e75589 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -30,6 +30,8 @@ public abstract class HttpService extends AbstractService { protected String contextPath; + protected int maxRequestSize; + @Override public void innerStart() throws Exception { if (this.apiServer != null) { @@ -64,8 +66,7 @@ protected void initServer() { protected ServletContextHandler initContextHandler() { ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); context.setContextPath(this.contextPath); - int maxMessageSize = Args.getInstance().getMaxMessageSize(); - SizeLimitHandler sizeLimitHandler = new SizeLimitHandler(maxMessageSize, -1); + SizeLimitHandler sizeLimitHandler = new SizeLimitHandler(this.maxRequestSize, -1); sizeLimitHandler.setHandler(context); this.apiServer.setHandler(sizeLimitHandler); return context; diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 0e71294d786..ce6ef8bd8b1 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -474,6 +474,20 @@ public static void applyConfigParams( PARAMETER.maxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) ? config.getInt(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + int defaultHttpMaxMessageSize = 4 * 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) { + throw new TronError("node.http.maxMessageSize must be positive, got: " + + PARAMETER.httpMaxMessageSize, PARAMETER_INIT); + } + PARAMETER.jsonRpcMaxMessageSize = config.hasPath(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) + ? config.getInt(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) : defaultHttpMaxMessageSize; + if (PARAMETER.jsonRpcMaxMessageSize <= 0) { + throw new TronError("node.jsonrpc.maxMessageSize must be positive, got: " + + PARAMETER.jsonRpcMaxMessageSize, PARAMETER_INIT); + } + PARAMETER.maxHeaderListSize = config.hasPath(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) ? config.getInt(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) : GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE; diff --git a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java index dbb872febce..e05d59accf1 100644 --- a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java +++ b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java @@ -136,6 +136,7 @@ private ConfigKey() { public static final String NODE_HTTP_SOLIDITY_ENABLE = "node.http.solidityEnable"; public static final String NODE_HTTP_PBFT_ENABLE = "node.http.PBFTEnable"; public static final String NODE_HTTP_PBFT_PORT = "node.http.PBFTPort"; + public static final String NODE_HTTP_MAX_MESSAGE_SIZE = "node.http.maxMessageSize"; // node - jsonrpc public static final String NODE_JSONRPC_HTTP_FULLNODE_ENABLE = @@ -150,6 +151,7 @@ private ConfigKey() { public static final String NODE_JSONRPC_MAX_SUB_TOPICS = "node.jsonrpc.maxSubTopics"; public static final String NODE_JSONRPC_MAX_BLOCK_FILTER_NUM = "node.jsonrpc.maxBlockFilterNum"; + public static final String NODE_JSONRPC_MAX_MESSAGE_SIZE = "node.jsonrpc.maxMessageSize"; // node - dns public static final String NODE_DNS_TREE_URLS = "node.dns.treeUrls"; diff --git a/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java b/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java index 3ad4ace62fc..5a3b86cb396 100644 --- a/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java +++ b/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java @@ -297,6 +297,7 @@ public FullNodeHttpApiService() { port = Args.getInstance().getFullNodeHttpPort(); enable = isFullNode() && Args.getInstance().isFullNodeHttpEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java b/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java index 359adfc2b39..0c4843c0550 100644 --- a/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java +++ b/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java @@ -170,6 +170,7 @@ public SolidityNodeHttpApiService() { port = Args.getInstance().getSolidityHttpPort(); enable = !isFullNode() && Args.getInstance().isSolidityNodeHttpEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java index fffaf8d4e7b..5282ef5c819 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java +++ b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java @@ -19,6 +19,7 @@ public JsonRpcServiceOnPBFT() { port = Args.getInstance().getJsonRpcHttpPBFTPort(); enable = isFullNode() && Args.getInstance().isJsonRpcHttpPBFTNodeEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getJsonRpcMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java index a6f7d5dd5e7..8b52066d5f8 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java +++ b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java @@ -19,6 +19,7 @@ public JsonRpcServiceOnSolidity() { port = Args.getInstance().getJsonRpcHttpSolidityPort(); enable = isFullNode() && Args.getInstance().isJsonRpcHttpSolidityNodeEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getJsonRpcMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java index a77b45353c9..c0616c2ae78 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java @@ -173,6 +173,7 @@ public HttpApiOnPBFTService() { port = Args.getInstance().getPBFTHttpPort(); enable = isFullNode() && Args.getInstance().isPBFTHttpEnable(); contextPath = "/walletpbft"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java index f69597959f8..33e325bd578 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java @@ -181,6 +181,7 @@ public HttpApiOnSolidityService() { port = Args.getInstance().getSolidityHttpPort(); enable = isFullNode() && Args.getInstance().isSolidityNodeHttpEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java b/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java index 566ad33a722..ffe81bfa100 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java @@ -24,6 +24,7 @@ public FullNodeJsonRpcHttpService() { port = Args.getInstance().getJsonRpcHttpFullNodePort(); enable = isFullNode() && Args.getInstance().isJsonRpcHttpFullNodeEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getJsonRpcMaxMessageSize(); } @Override diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index 2d174f14629..dbd6b1eb97e 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -3,7 +3,6 @@ import com.google.common.io.ByteStreams; import java.io.IOException; import java.net.URI; -import java.nio.file.Files; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -21,7 +20,9 @@ import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.tron.common.TestConstants; import org.tron.common.application.HttpService; import org.tron.common.utils.PublicMethod; @@ -29,27 +30,30 @@ /** * Tests the {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size - * enforcement configured in {@link HttpService initContextHandler()}. + * enforcement configured in {@link HttpService#initContextHandler()}. * - *

Unlike a standalone Jetty micro-server, this test creates a concrete - * {@link HttpService} subclass and calls {@link HttpService#start()}, so the - * production code paths of {@code initServer()} and {@code initContextHandler()} - * are exercised directly and reflected in code-coverage reports.

- * - *

Key behaviours proven:

+ *

Covers:

*
    *
  • Bodies within the limit are accepted ({@code 200}).
  • *
  • Bodies exceeding the limit are rejected ({@code 413}).
  • *
  • The limit counts raw UTF-8 bytes, not Java {@code char}s.
  • + *
  • HTTP and JSON-RPC services use independent size limits.
  • + *
  • Default values are 4x {@code GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE}.
  • *
*/ @Slf4j public class SizeLimitHandlerTest { - private static final int MAX_BODY_SIZE = 1024; + private static final int HTTP_MAX_BODY_SIZE = 1024; + private static final int JSONRPC_MAX_BODY_SIZE = 512; + + @ClassRule + public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); private static TestHttpService httpService; - private static URI serverUri; + private static TestJsonRpcService jsonRpcService; + private static URI httpServerUri; + private static URI jsonRpcServerUri; private static CloseableHttpClient client; /** Echoes the raw request-body bytes back so tests can inspect what arrived. */ @@ -63,11 +67,12 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } } - /** Minimal concrete {@link HttpService} that registers only an {@link EchoServlet}. */ + /** Minimal concrete {@link HttpService} wired with a given size limit. */ static class TestHttpService extends HttpService { - TestHttpService(int port) { + TestHttpService(int port, int maxRequestSize) { this.port = port; this.contextPath = "/"; + this.maxRequestSize = maxRequestSize; } @Override @@ -76,23 +81,37 @@ protected void addServlet(ServletContextHandler context) { } } - /** - * Initialises {@link Args} and starts a real {@link HttpService} whose - * {@code initServer()} and {@code initContextHandler()} are the production - * implementations — guaranteeing test coverage of the new - * {@code SizeLimitHandler} wiring. - */ + /** Minimal concrete {@link HttpService} simulating a JSON-RPC service. */ + static class TestJsonRpcService extends HttpService { + TestJsonRpcService(int port, int maxRequestSize) { + this.port = port; + this.contextPath = "/"; + this.maxRequestSize = maxRequestSize; + } + + @Override + protected void addServlet(ServletContextHandler context) { + context.addServlet(new ServletHolder(new EchoServlet()), "/jsonrpc"); + } + } + @BeforeClass public static void setup() throws Exception { - String dbPath = Files.createTempDirectory("sizelimit-test").toString(); - Args.setParam(new String[]{"--output-directory", dbPath}, TestConstants.TEST_CONF); - Args.getInstance().setMaxMessageSize(MAX_BODY_SIZE); + Args.setParam(new String[]{"-d", temporaryFolder.newFolder().toString()}, + TestConstants.TEST_CONF); + Args.getInstance().setHttpMaxMessageSize(HTTP_MAX_BODY_SIZE); + Args.getInstance().setJsonRpcMaxMessageSize(JSONRPC_MAX_BODY_SIZE); - int port = PublicMethod.chooseRandomPort(); - httpService = new TestHttpService(port); + int httpPort = PublicMethod.chooseRandomPort(); + httpService = new TestHttpService(httpPort, HTTP_MAX_BODY_SIZE); httpService.start().get(10, TimeUnit.SECONDS); + httpServerUri = new URI(String.format("http://localhost:%d/", httpPort)); + + int jsonRpcPort = PublicMethod.chooseRandomPort(); + jsonRpcService = new TestJsonRpcService(jsonRpcPort, JSONRPC_MAX_BODY_SIZE); + jsonRpcService.start().get(10, TimeUnit.SECONDS); + jsonRpcServerUri = new URI(String.format("http://localhost:%d/jsonrpc", jsonRpcPort)); - serverUri = new URI(String.format("http://localhost:%d/", port)); client = HttpClients.createDefault(); } @@ -103,30 +122,86 @@ public static void teardown() throws Exception { client.close(); } } finally { - if (httpService != null) { - httpService.stop(); + try { + if (httpService != null) { + httpService.stop(); + } + } finally { + if (jsonRpcService != null) { + jsonRpcService.stop(); + } } Args.clearParam(); } } - // -- body-size tests (covers HttpService.initContextHandler) --------------- + // -- HTTP service body-size tests ------------------------------------------- + + @Test + public void testHttpBodyWithinLimit() throws Exception { + Assert.assertEquals(200, post(httpServerUri, new StringEntity("small body"))); + } + + @Test + public void testHttpBodyExceedsLimit() throws Exception { + Assert.assertEquals(413, + post(httpServerUri, new StringEntity(repeat('a', HTTP_MAX_BODY_SIZE + 1)))); + } + + @Test + public void testHttpBodyAtExactLimit() throws Exception { + Assert.assertEquals(200, + post(httpServerUri, new StringEntity(repeat('b', HTTP_MAX_BODY_SIZE)))); + } + + // -- JSON-RPC service body-size tests --------------------------------------- @Test - public void testBodyWithinLimit() throws Exception { - Assert.assertEquals(200, post(new StringEntity("small body"))); + public void testJsonRpcBodyWithinLimit() throws Exception { + Assert.assertEquals(200, + post(jsonRpcServerUri, new StringEntity("{\"method\":\"eth_blockNumber\"}"))); } @Test - public void testBodyExceedsLimit() throws Exception { - Assert.assertEquals(413, post(new StringEntity(repeat('a', MAX_BODY_SIZE + 1)))); + public void testJsonRpcBodyExceedsLimit() throws Exception { + Assert.assertEquals(413, + post(jsonRpcServerUri, new StringEntity(repeat('x', JSONRPC_MAX_BODY_SIZE + 1)))); + } + + @Test + public void testJsonRpcBodyAtExactLimit() throws Exception { + Assert.assertEquals(200, + post(jsonRpcServerUri, new StringEntity(repeat('c', JSONRPC_MAX_BODY_SIZE)))); + } + + // -- Independent limit tests ------------------------------------------------ + + @Test + public void testHttpAndJsonRpcHaveIndependentLimits() throws Exception { + // A body that exceeds JSON-RPC limit but is within HTTP limit + String body = repeat('d', JSONRPC_MAX_BODY_SIZE + 100); + Assert.assertTrue(body.length() < HTTP_MAX_BODY_SIZE); + + Assert.assertEquals(200, post(httpServerUri, new StringEntity(body))); + Assert.assertEquals(413, post(jsonRpcServerUri, new StringEntity(body))); + } + + // -- UTF-8 byte counting test ----------------------------------------------- + + @Test + public void testLimitIsBasedOnBytesNotCharacters() throws Exception { + // Each CJK character is 3 UTF-8 bytes; 342 chars x 3 = 1026 bytes > 1024 + String cjk = repeat('\u4e00', 342); + Assert.assertEquals(342, cjk.length()); + Assert.assertEquals(1026, cjk.getBytes("UTF-8").length); + Assert.assertEquals(413, post(httpServerUri, new StringEntity(cjk, "UTF-8"))); } - // -- helpers --------------------------------------------------------------- + // -- helpers ---------------------------------------------------------------- /** POSTs with the given entity and returns the HTTP status code. */ - private int post(HttpEntity entity) throws Exception { - HttpPost req = new HttpPost(serverUri); + private int post(URI uri, HttpEntity entity) throws Exception { + HttpPost req = new HttpPost(uri); req.setEntity(entity); HttpResponse resp = client.execute(req); EntityUtils.consume(resp.getEntity()); diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index a4ce9a5030e..5ec388ebcd8 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -120,6 +120,9 @@ public void get() { Assert.assertEquals(60000L, parameter.getMaxConnectionIdleInMillis()); Assert.assertEquals(Long.MAX_VALUE, parameter.getMaxConnectionAgeInMillis()); Assert.assertEquals(GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getMaxMessageSize()); + Assert.assertEquals(4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getHttpMaxMessageSize()); + Assert.assertEquals(4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, + parameter.getJsonRpcMaxMessageSize()); Assert.assertEquals(GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, parameter.getMaxHeaderListSize()); Assert.assertEquals(1L, parameter.getAllowCreationOfContracts()); Assert.assertEquals(0, parameter.getConsensusLogicOptimization()); From c41d30ebcb24e6652c11aef2686539620fc02ad2 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 1 Apr 2026 20:52:39 +0800 Subject: [PATCH 04/13] opt(checkstyle): optimize checkstyle --- .../test/java/org/tron/common/jetty/SizeLimitHandlerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index dbd6b1eb97e..e12b6ea57ad 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -30,7 +30,7 @@ /** * Tests the {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size - * enforcement configured in {@link HttpService#initContextHandler()}. + * enforcement configured in {@link HttpService initContextHandler()}. * *

Covers:

*
    @@ -191,7 +191,7 @@ public void testHttpAndJsonRpcHaveIndependentLimits() throws Exception { @Test public void testLimitIsBasedOnBytesNotCharacters() throws Exception { // Each CJK character is 3 UTF-8 bytes; 342 chars x 3 = 1026 bytes > 1024 - String cjk = repeat('\u4e00', 342); + String cjk = repeat('一', 342); Assert.assertEquals(342, cjk.length()); Assert.assertEquals(1026, cjk.getBytes("UTF-8").length); Assert.assertEquals(413, post(httpServerUri, new StringEntity(cjk, "UTF-8"))); From 321e26a194908fc99aa3ea127a00511d1546c754 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 2 Apr 2026 09:45:59 +0800 Subject: [PATCH 05/13] change(config): update default size --- framework/src/main/java/org/tron/core/config/args/Args.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index ce6ef8bd8b1..0ff2071c05f 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -474,7 +474,7 @@ public static void applyConfigParams( PARAMETER.maxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) ? config.getInt(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; - int defaultHttpMaxMessageSize = 4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + 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) { From e6b11a344e4d70e431e5264d37ab2c1f647aab65 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 2 Apr 2026 16:08:22 +0800 Subject: [PATCH 06/13] test(framework): align ArgsTest with 4M defaults --- .../src/test/java/org/tron/core/config/args/ArgsTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 5ec388ebcd8..b38dceaebe5 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -120,9 +120,8 @@ public void get() { Assert.assertEquals(60000L, parameter.getMaxConnectionIdleInMillis()); Assert.assertEquals(Long.MAX_VALUE, parameter.getMaxConnectionAgeInMillis()); Assert.assertEquals(GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getMaxMessageSize()); - Assert.assertEquals(4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getHttpMaxMessageSize()); - Assert.assertEquals(4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, - parameter.getJsonRpcMaxMessageSize()); + Assert.assertEquals(GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getHttpMaxMessageSize()); + Assert.assertEquals(GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getJsonRpcMaxMessageSize()); Assert.assertEquals(GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, parameter.getMaxHeaderListSize()); Assert.assertEquals(1L, parameter.getAllowCreationOfContracts()); Assert.assertEquals(0, parameter.getConsensusLogicOptimization()); @@ -345,4 +344,3 @@ public void testConfigStorageDefaults() { Args.clearParam(); } } - From 674e547b4f749b02b988a158badb1b313792b0d9 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 3 Apr 2026 15:48:08 +0800 Subject: [PATCH 07/13] test(http): doc for default value --- .../test/java/org/tron/common/jetty/SizeLimitHandlerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index e12b6ea57ad..a2c4c3f369c 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -38,7 +38,7 @@ *
  • Bodies exceeding the limit are rejected ({@code 413}).
  • *
  • The limit counts raw UTF-8 bytes, not Java {@code char}s.
  • *
  • HTTP and JSON-RPC services use independent size limits.
  • - *
  • Default values are 4x {@code GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE}.
  • + *
  • Default values are {@code GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE} (4 MB).
  • *
*/ @Slf4j From 30487507558f2c70bfc652c7186cf79632d66fb9 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 9 Apr 2026 18:34:59 +0800 Subject: [PATCH 08/13] fix(api): use httpMaxMessageSize in checkBodySize instead of gRPC limit 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 --- .../org/tron/core/services/http/Util.java | 5 ++-- .../org/tron/core/services/http/UtilTest.java | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index f112e13c818..1fefdde5e91 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -330,8 +330,9 @@ public static Transaction packTransaction(String strTransaction, boolean selfTyp @Deprecated public static void checkBodySize(String body) throws Exception { CommonParameter parameter = Args.getInstance(); - if (body.getBytes().length > parameter.getMaxMessageSize()) { - throw new Exception("body size is too big, the limit is " + parameter.getMaxMessageSize()); + if (body.getBytes().length > parameter.getHttpMaxMessageSize()) { + throw new Exception("body size is too big, the limit is " + + parameter.getHttpMaxMessageSize()); } } diff --git a/framework/src/test/java/org/tron/core/services/http/UtilTest.java b/framework/src/test/java/org/tron/core/services/http/UtilTest.java index 98c11fd4018..0a8337fee66 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilTest.java @@ -129,6 +129,32 @@ public void testPackTransactionWithInvalidType() { txSignWeight.getResult().getMessage()); } + @Test + public void testCheckBodySizeUsesHttpLimit() throws Exception { + int originalHttpMax = Args.getInstance().getHttpMaxMessageSize(); + int originalRpcMax = Args.getInstance().getMaxMessageSize(); + try { + // set httpMaxMessageSize larger than maxMessageSize + Args.getInstance().setHttpMaxMessageSize(200); + Args.getInstance().setMaxMessageSize(100); + + String withinHttpLimit = new String(new char[150]).replace('\0', 'a'); + // should pass: 150 < httpMaxMessageSize(200), even though > maxMessageSize(100) + Util.checkBodySize(withinHttpLimit); + + String exceedsHttpLimit = new String(new char[201]).replace('\0', 'b'); + try { + Util.checkBodySize(exceedsHttpLimit); + Assert.fail("expected exception for body exceeding httpMaxMessageSize"); + } catch (Exception e) { + Assert.assertTrue(e.getMessage().contains("200")); + } + } finally { + Args.getInstance().setHttpMaxMessageSize(originalHttpMax); + Args.getInstance().setMaxMessageSize(originalRpcMax); + } + } + @Test public void testPackTransaction() { String strTransaction = "{\n" From 5e35c4cec3660d67dd0f1cf910b6b1707554a9c7 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 17:22:56 +0800 Subject: [PATCH 09/13] test(http): add chunked transfer and zero-limit tests for SizeLimitHandler Add tests to cover scenarios raised in PR review: - Chunked (no Content-Length) requests within/exceeding the limit - Servlet broad catch(Exception) absorbing streaming BadMessageException - SizeLimitHandler behavior when limit is 0 (rejects all non-empty bodies) Replace EchoServlet with BroadCatchServlet to mirror real servlet chain. Co-Authored-By: Claude Opus 4.6 --- .../common/jetty/SizeLimitHandlerTest.java | 97 +++++++++++++++++-- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index a2c4c3f369c..12cd78c3b9a 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -1,9 +1,10 @@ package org.tron.common.jetty; -import com.google.common.io.ByteStreams; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -11,6 +12,7 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; @@ -56,14 +58,25 @@ public class SizeLimitHandlerTest { private static URI jsonRpcServerUri; private static CloseableHttpClient client; - /** Echoes the raw request-body bytes back so tests can inspect what arrived. */ - public static class EchoServlet extends HttpServlet { + /** + * Simulates the real servlet pattern: reads body via getReader(), wraps in + * broad catch(Exception) — mirrors what RateLimiterServlet + actual servlets do. + */ + public static class BroadCatchServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { - byte[] body = ByteStreams.toByteArray(req.getInputStream()); - resp.setStatus(HttpServletResponse.SC_OK); - resp.setContentType("application/octet-stream"); - resp.getOutputStream().write(body); + try { + String body = req.getReader().lines() + .collect(Collectors.joining(System.lineSeparator())); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + resp.getWriter().println("{\"size\":" + body.length() + "}"); + } catch (Exception e) { + // Mimics RateLimiterServlet line 119-120: silently logs, does not rethrow + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + resp.getWriter().println("{\"Error\":\"" + e.getClass().getSimpleName() + "\"}"); + } } } @@ -77,7 +90,7 @@ static class TestHttpService extends HttpService { @Override protected void addServlet(ServletContextHandler context) { - context.addServlet(new ServletHolder(new EchoServlet()), "/*"); + context.addServlet(new ServletHolder(new BroadCatchServlet()), "/*"); } } @@ -91,7 +104,7 @@ static class TestJsonRpcService extends HttpService { @Override protected void addServlet(ServletContextHandler context) { - context.addServlet(new ServletHolder(new EchoServlet()), "/jsonrpc"); + context.addServlet(new ServletHolder(new BroadCatchServlet()), "/jsonrpc"); } } @@ -197,6 +210,72 @@ public void testLimitIsBasedOnBytesNotCharacters() throws Exception { Assert.assertEquals(413, post(httpServerUri, new StringEntity(cjk, "UTF-8"))); } + // -- Chunked (no Content-Length) transfer tests ------------------------------ + + /** + * Chunked request within the limit should succeed (EchoServlet). + * InputStreamEntity with size=-1 sends chunked Transfer-Encoding (no Content-Length). + */ + @Test + public void testChunkedBodyWithinLimit() throws Exception { + byte[] data = repeat('a', HTTP_MAX_BODY_SIZE / 4).getBytes("UTF-8"); + InputStreamEntity chunked = new InputStreamEntity(new ByteArrayInputStream(data), -1); + Assert.assertEquals(200, post(httpServerUri, chunked)); + } + + /** + * Chunked oversized body hitting a servlet with broad catch(Exception). + * + *

SizeLimitHandler's LimitInterceptor throws BadMessageException during + * streaming read, but the servlet's catch(Exception) absorbs it and returns + * 200 + error JSON instead of 413. This matches real TRON servlet behavior. + * + *

OOM protection still works: the body read is truncated at the limit. + */ + @Test + public void testChunkedBodyExceedsLimit() throws Exception { + byte[] data = repeat('a', HTTP_MAX_BODY_SIZE * 2).getBytes("UTF-8"); + InputStreamEntity chunked = new InputStreamEntity(new ByteArrayInputStream(data), -1); + HttpPost req = new HttpPost(httpServerUri); + req.setEntity(chunked); + HttpResponse resp = client.execute(req); + int status = resp.getStatusLine().getStatusCode(); + String body = EntityUtils.toString(resp.getEntity()); + logger.info("Chunked oversized: status={}, body={}", status, body); + + // catch(Exception) absorbs BadMessageException → 200 + error JSON, not 413. + // Body read IS truncated — OOM protection still effective. + Assert.assertEquals(200, status); + Assert.assertTrue("Error should be surfaced in response body", + body.contains("Error")); + } + + // -- Zero-limit behavior test ----------------------------------------------- + + /** + * When maxRequestSize is 0, SizeLimitHandler treats it as "reject all bodies > 0 bytes". + * Jetty's logic: {@code _requestLimit >= 0 && size > _requestLimit} — 0 >= 0 is true, + * so any non-empty body triggers 413. This is NOT "pass all" — it is a silent DoS + * against the node's own API. + */ + @Test + public void testZeroLimitRejectsAllBodies() throws Exception { + int zeroPort = PublicMethod.chooseRandomPort(); + TestHttpService zeroService = new TestHttpService(zeroPort, 0); + try { + zeroService.start().get(10, TimeUnit.SECONDS); + URI zeroUri = new URI(String.format("http://localhost:%d/", zeroPort)); + + // Empty body should pass (0 is NOT > 0) + Assert.assertEquals(200, post(zeroUri, new StringEntity(""))); + + // Any non-empty body should be rejected + Assert.assertEquals(413, post(zeroUri, new StringEntity("x"))); + } finally { + zeroService.stop(); + } + } + // -- helpers ---------------------------------------------------------------- /** POSTs with the given entity and returns the HTTP status code. */ From aa34bfce22cbf85f8f76d2ffeb74cdf044f468a7 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:02:31 +0800 Subject: [PATCH 10/13] refactor(config): use getMemorySize() for size limit configs Replace getInt() with getMemorySize().toBytes() for maxMessageSize, httpMaxMessageSize and jsonRpcMaxMessageSize config parsing. This enables human-readable size values (e.g. 4m, 128MB) while maintaining backward compatibility with raw byte values. - maxMessageSize (gRPC): keep int field, validate <= Integer.MAX_VALUE for gRPC API compatibility, add positive value validation - httpMaxMessageSize / jsonRpcMaxMessageSize: widen to long, matching Jetty SizeLimitHandler's long parameter type - Add config.conf documentation for all three size parameters Co-Authored-By: Claude Opus 4.6 --- .../tron/common/parameter/CommonParameter.java | 4 ++-- .../tron/common/application/HttpService.java | 2 +- .../java/org/tron/core/config/args/Args.java | 18 +++++++++++++----- framework/src/main/resources/config.conf | 13 +++++++++++-- .../common/jetty/SizeLimitHandlerTest.java | 4 ++-- .../org/tron/core/services/http/UtilTest.java | 2 +- 6 files changed, 30 insertions(+), 13 deletions(-) diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index abb93765050..8a8a88df617 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -216,10 +216,10 @@ public class CommonParameter { public int maxMessageSize; @Getter @Setter - public int httpMaxMessageSize; + public long httpMaxMessageSize; @Getter @Setter - public int jsonRpcMaxMessageSize; + public long jsonRpcMaxMessageSize; @Getter @Setter public int maxHeaderListSize; diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index b7032e75589..388a524b633 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -30,7 +30,7 @@ public abstract class HttpService extends AbstractService { protected String contextPath; - protected int maxRequestSize; + protected long maxRequestSize; @Override public void innerStart() throws Exception { diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 0ff2071c05f..bed7be3004b 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -471,18 +471,26 @@ public static void applyConfigParams( ? config.getLong(ConfigKey.NODE_RPC_MAX_CONNECTION_AGE_IN_MILLIS) : Long.MAX_VALUE; - PARAMETER.maxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) - ? config.getInt(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + long rpcMaxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) + ? config.getMemorySize(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE).toBytes() + : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + if (rpcMaxMessageSize <= 0 || rpcMaxMessageSize > Integer.MAX_VALUE) { + throw new TronError("node.rpc.maxMessageSize must be positive and <= " + + Integer.MAX_VALUE + ", got: " + rpcMaxMessageSize, PARAMETER_INIT); + } + PARAMETER.maxMessageSize = (int) rpcMaxMessageSize; - int defaultHttpMaxMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + long 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; + ? config.getMemorySize(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE).toBytes() + : defaultHttpMaxMessageSize; if (PARAMETER.httpMaxMessageSize <= 0) { throw new TronError("node.http.maxMessageSize must be positive, got: " + PARAMETER.httpMaxMessageSize, PARAMETER_INIT); } PARAMETER.jsonRpcMaxMessageSize = config.hasPath(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) - ? config.getInt(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) : defaultHttpMaxMessageSize; + ? config.getMemorySize(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE).toBytes() + : defaultHttpMaxMessageSize; if (PARAMETER.jsonRpcMaxMessageSize <= 0) { throw new TronError("node.jsonrpc.maxMessageSize must be positive, got: " + PARAMETER.jsonRpcMaxMessageSize, PARAMETER_INIT); diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 661a592e431..72e00fdbab0 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -223,6 +223,10 @@ node { solidityPort = 8091 PBFTEnable = true PBFTPort = 8092 + + # The maximum request body size for HTTP API, default 4M (4194304 bytes). + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # maxMessageSize = 4m } rpc { @@ -248,8 +252,9 @@ node { # Connection lasting longer than which will be gracefully terminated # maxConnectionAgeInMillis = - # The maximum message size allowed to be received on the server, default 4MB - # maxMessageSize = + # The maximum message size allowed to be received on the server, default 4M (4194304 bytes). + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # maxMessageSize = 4m # The maximum size of header list allowed to be received, default 8192 # maxHeaderListSize = @@ -357,6 +362,10 @@ node { # openHistoryQueryWhenLiteFN = false jsonrpc { + # The maximum request body size for JSON-RPC API, default 4M (4194304 bytes). + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # maxMessageSize = 4m + # Note: Before release_4.8.1, if you turn on jsonrpc and run it for a while and then turn it off, # you will not be able to get the data from eth_getLogs for that period of time. Default: false # httpFullNodeEnable = false diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index 12cd78c3b9a..13c840fc2e3 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -82,7 +82,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I /** Minimal concrete {@link HttpService} wired with a given size limit. */ static class TestHttpService extends HttpService { - TestHttpService(int port, int maxRequestSize) { + TestHttpService(int port, long maxRequestSize) { this.port = port; this.contextPath = "/"; this.maxRequestSize = maxRequestSize; @@ -96,7 +96,7 @@ protected void addServlet(ServletContextHandler context) { /** Minimal concrete {@link HttpService} simulating a JSON-RPC service. */ static class TestJsonRpcService extends HttpService { - TestJsonRpcService(int port, int maxRequestSize) { + TestJsonRpcService(int port, long maxRequestSize) { this.port = port; this.contextPath = "/"; this.maxRequestSize = maxRequestSize; diff --git a/framework/src/test/java/org/tron/core/services/http/UtilTest.java b/framework/src/test/java/org/tron/core/services/http/UtilTest.java index 0a8337fee66..24a8a2bdef3 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilTest.java @@ -131,7 +131,7 @@ public void testPackTransactionWithInvalidType() { @Test public void testCheckBodySizeUsesHttpLimit() throws Exception { - int originalHttpMax = Args.getInstance().getHttpMaxMessageSize(); + long originalHttpMax = Args.getInstance().getHttpMaxMessageSize(); int originalRpcMax = Args.getInstance().getMaxMessageSize(); try { // set httpMaxMessageSize larger than maxMessageSize From c64d3b26a940f5d31ca5718203efafa33dd6e4f2 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:06:31 +0800 Subject: [PATCH 11/13] fix(config): allow zero value for maxMessageSize parameters Change validation from <= 0 to < 0 for all three size limit configs. Zero is a valid value meaning "reject all non-empty request bodies", with consistent semantics in both gRPC (maxInboundMessageSize >= 0) and Jetty SizeLimitHandler (limit >= 0 && size > limit). Co-Authored-By: Claude Opus 4.6 --- .../main/java/org/tron/core/config/args/Args.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index bed7be3004b..c7d58947173 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -474,8 +474,8 @@ public static void applyConfigParams( long rpcMaxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE).toBytes() : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; - if (rpcMaxMessageSize <= 0 || rpcMaxMessageSize > Integer.MAX_VALUE) { - throw new TronError("node.rpc.maxMessageSize must be positive and <= " + if (rpcMaxMessageSize < 0 || rpcMaxMessageSize > Integer.MAX_VALUE) { + throw new TronError("node.rpc.maxMessageSize must be non-negative and <= " + Integer.MAX_VALUE + ", got: " + rpcMaxMessageSize, PARAMETER_INIT); } PARAMETER.maxMessageSize = (int) rpcMaxMessageSize; @@ -484,15 +484,15 @@ public static void applyConfigParams( PARAMETER.httpMaxMessageSize = config.hasPath(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE).toBytes() : defaultHttpMaxMessageSize; - if (PARAMETER.httpMaxMessageSize <= 0) { - throw new TronError("node.http.maxMessageSize must be positive, got: " + if (PARAMETER.httpMaxMessageSize < 0) { + throw new TronError("node.http.maxMessageSize must be non-negative, got: " + PARAMETER.httpMaxMessageSize, PARAMETER_INIT); } PARAMETER.jsonRpcMaxMessageSize = config.hasPath(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE).toBytes() : defaultHttpMaxMessageSize; - if (PARAMETER.jsonRpcMaxMessageSize <= 0) { - throw new TronError("node.jsonrpc.maxMessageSize must be positive, got: " + if (PARAMETER.jsonRpcMaxMessageSize < 0) { + throw new TronError("node.jsonrpc.maxMessageSize must be non-negative, got: " + PARAMETER.jsonRpcMaxMessageSize, PARAMETER_INIT); } From 5e3eed022cb4c3309eb47c23908218d1b087ed0e Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:25:34 +0800 Subject: [PATCH 12/13] test(http): verify checkBodySize consistency with SizeLimitHandler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests proving the two enforcement layers measure identical byte counts for normal JSON payloads (ASCII and UTF-8). For bodies with \r\n line endings, checkBodySize measures fewer bytes than wire — a safe direction that never causes false rejections. Co-Authored-By: Claude Opus 4.6 --- .../common/jetty/SizeLimitHandlerTest.java | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index 13c840fc2e3..7c264cc8fdb 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -1,5 +1,6 @@ package org.tron.common.jetty; +import com.alibaba.fastjson.JSONObject; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; @@ -70,7 +71,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I .collect(Collectors.joining(System.lineSeparator())); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentType("application/json"); - resp.getWriter().println("{\"size\":" + body.length() + "}"); + resp.getWriter().println("{\"size\":" + body.length() + + ",\"bytes\":" + body.getBytes().length + "}"); } catch (Exception e) { // Mimics RateLimiterServlet line 119-120: silently logs, does not rethrow resp.setStatus(HttpServletResponse.SC_OK); @@ -276,8 +278,76 @@ public void testZeroLimitRejectsAllBodies() throws Exception { } } + // -- checkBodySize vs SizeLimitHandler consistency tests -------------------- + + /** + * For pure ASCII JSON (the normal TRON API case), wire bytes and + * {@code body.getBytes().length} (what {@code Util.checkBodySize()} measures) + * must be identical — the two enforcement layers agree exactly. + */ + @Test + public void testWireBytesMatchCheckBodySizeForAsciiJson() throws Exception { + String jsonBody = "{\"owner_address\":\"TN3zfjYUmMFK3ZsHSsrdJoNRtGkQmZLBLz\"" + + ",\"amount\":1000000}"; + int wireBytes = jsonBody.getBytes("UTF-8").length; + + String respBody = postForBody(httpServerUri, new StringEntity(jsonBody, "UTF-8")); + JSONObject json = JSONObject.parseObject(respBody); + int servletBytes = json.getIntValue("bytes"); + + Assert.assertEquals("wire bytes should equal checkBodySize for ASCII JSON", + wireBytes, servletBytes); + } + + /** + * For UTF-8 JSON with multi-byte characters (CJK), wire bytes and + * {@code body.getBytes().length} must still be identical — UTF-8 round-trips + * through {@code request.getReader()} → {@code String.getBytes()} losslessly. + */ + @Test + public void testWireBytesMatchCheckBodySizeForUtf8Json() throws Exception { + String jsonBody = "{\"name\":\"测试地址\",\"amount\":100}"; + int wireBytes = jsonBody.getBytes("UTF-8").length; + + String respBody = postForBody(httpServerUri, new StringEntity(jsonBody, "UTF-8")); + JSONObject json = JSONObject.parseObject(respBody); + int servletBytes = json.getIntValue("bytes"); + + Assert.assertEquals("wire bytes should equal checkBodySize for UTF-8 JSON", + wireBytes, servletBytes); + } + + /** + * When the body contains {@code \r\n} line endings, {@code lines().collect()} + * normalizes them to {@code \n} (on Linux) or the platform line separator. + * This makes {@code checkBodySize} measure fewer bytes than the wire — + * a safe direction: checkBodySize never rejects what SizeLimitHandler accepts. + */ + @Test + public void testCheckBodySizeSafeDirectionWithNewlines() throws Exception { + String body = "{\"key1\":\"value1\",\r\n\"key2\":\"value2\",\r\n\"key3\":\"value3\"}"; + int wireBytes = body.getBytes("UTF-8").length; + + String respBody = postForBody(httpServerUri, new StringEntity(body, "UTF-8")); + JSONObject json = JSONObject.parseObject(respBody); + int servletBytes = json.getIntValue("bytes"); + + Assert.assertTrue("checkBodySize bytes <= wire bytes (safe direction)", + servletBytes <= wireBytes); + logger.info("Newline test: wire={}, servlet={}, diff={}", + wireBytes, servletBytes, wireBytes - servletBytes); + } + // -- helpers ---------------------------------------------------------------- + /** POSTs with the given entity and returns the response body as a string. */ + private String postForBody(URI uri, HttpEntity entity) throws Exception { + HttpPost req = new HttpPost(uri); + req.setEntity(entity); + HttpResponse resp = client.execute(req); + return EntityUtils.toString(resp.getEntity()); + } + /** POSTs with the given entity and returns the HTTP status code. */ private int post(URI uri, HttpEntity entity) throws Exception { HttpPost req = new HttpPost(uri); From de76e5f1bc02353eeafc4b7c12e50c5618b9c204 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:37:26 +0800 Subject: [PATCH 13/13] fix(config): correct comment from "positive" to "non-negative" Zero is a valid value for all three maxMessageSize parameters, so the config documentation should say "non-negative" not "positive". Co-Authored-By: Claude Opus 4.6 --- framework/src/main/resources/config.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 72e00fdbab0..6873e11364a 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -225,7 +225,7 @@ node { PBFTPort = 8092 # The maximum request body size for HTTP API, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. # maxMessageSize = 4m } @@ -253,7 +253,7 @@ node { # maxConnectionAgeInMillis = # The maximum message size allowed to be received on the server, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. # maxMessageSize = 4m # The maximum size of header list allowed to be received, default 8192 @@ -363,7 +363,7 @@ node { jsonrpc { # The maximum request body size for JSON-RPC API, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. # maxMessageSize = 4m # Note: Before release_4.8.1, if you turn on jsonrpc and run it for a while and then turn it off,