From aa8006d3c92c5960e02ead188e58f6ecbedcccf3 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 19 Mar 2026 15:21:24 +0800 Subject: [PATCH 01/19] 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 e81cef505295cfbec9a237d800d7de0df431e83b Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 19 Mar 2026 15:55:39 +0800 Subject: [PATCH 02/19] 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 9fc6a29d4dccfefcac5c102ced1c37b865f85af2 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 23 Mar 2026 18:31:57 +0800 Subject: [PATCH 03/19] 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 a73158a718a..739db868281 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 83d7fd2c63d..65cb54bc843 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 @@ -526,6 +526,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 b21c9c440a4..e8a5c504d57 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 88d893bcf97..a1851f87550 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 @@ -119,6 +119,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 adb49d6f8057ee7f9f8e97c34901ec45fda78919 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 1 Apr 2026 20:52:39 +0800 Subject: [PATCH 04/19] 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 2cc14e25d2e68bf3729612bb6af73e78ef0c41d1 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 2 Apr 2026 09:45:59 +0800 Subject: [PATCH 05/19] 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 65cb54bc843..0f2d4c9190b 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 @@ -526,7 +526,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 e182fb27efea99905b6c937dfc8478c5af03a6dc Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 2 Apr 2026 16:08:22 +0800 Subject: [PATCH 06/19] 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 a1851f87550..1bed3e1c6ad 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 @@ -119,9 +119,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()); @@ -364,4 +363,3 @@ public void testConfigStorageDefaults() { Args.clearParam(); } } - From e0df7d4768245d6eeeabcc547ded0f6976540b76 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 3 Apr 2026 15:48:08 +0800 Subject: [PATCH 07/19] 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 baa4d88431e108127a1d341813e8eedb37fb12bb Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 9 Apr 2026 18:34:59 +0800 Subject: [PATCH 08/19] 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. --- .../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 cd1ebad625a6586591213aab4e4af4b234af2d0b Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 17:22:56 +0800 Subject: [PATCH 09/19] 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. --- .../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 8c539d13347f4b40238c8c9d76f59427e08a0417 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:02:31 +0800 Subject: [PATCH 10/19] 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 --- .../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 739db868281..53c5d4cfaca 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 0f2d4c9190b..c7738ca80d2 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 @@ -523,18 +523,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 369924074bc..abb6b2f7ec2 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 757470c9c0cb6a1e6be7ed3f082fc7d1cf598781 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:06:31 +0800 Subject: [PATCH 11/19] 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). --- .../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 c7738ca80d2..bc30a1f0a39 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 @@ -526,8 +526,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; @@ -536,15 +536,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 0b45cbbae92fe1c2b7b03b3cb3ca631719704ef9 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:25:34 +0800 Subject: [PATCH 12/19] 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. --- .../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 c28405ca5465a33721e4cc36faa3df9285f44828 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:37:26 +0800 Subject: [PATCH 13/19] 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". --- 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 abb6b2f7ec2..dcd399efa83 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, From c8bacc838ce0b6893225c2f7f8096129d8d78753 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 14 Apr 2026 17:55:42 +0800 Subject: [PATCH 14/19] fix(http): add safe default for maxRequestSize and fix misleading variable name Initialize HttpService.maxRequestSize to 4MB so future subclasses that omit the assignment get a safe limit instead of 0 (reject all requests). Rename defaultHttpMaxMessageSize to defaultMaxMessageSize since it serves as the fallback for both HTTP and JSON-RPC limits. --- .../main/java/org/tron/common/application/HttpService.java | 2 +- framework/src/main/java/org/tron/core/config/args/Args.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 388a524b633..66a21a8a4ed 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 long maxRequestSize; + protected long maxRequestSize = 4 * 1024 * 1024; // 4MB @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 bc30a1f0a39..ff20ed30241 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 @@ -532,17 +532,17 @@ public static void applyConfigParams( } PARAMETER.maxMessageSize = (int) rpcMaxMessageSize; - long defaultHttpMaxMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + long defaultMaxMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; PARAMETER.httpMaxMessageSize = config.hasPath(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE).toBytes() - : defaultHttpMaxMessageSize; + : defaultMaxMessageSize; 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; + : defaultMaxMessageSize; if (PARAMETER.jsonRpcMaxMessageSize < 0) { throw new TronError("node.jsonrpc.maxMessageSize must be non-negative, got: " + PARAMETER.jsonRpcMaxMessageSize, PARAMETER_INIT); From 8938c520ccea0ba0b6364faf90e8bfb57cc1654f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 14 Apr 2026 18:31:46 +0800 Subject: [PATCH 15/19] test(config): add maxMessageSize parsing and zero-value documentation Add comprehensive ArgsTest coverage for getMemorySize() parsing: - Human-readable sizes across KB/MB/GB tiers (binary and SI units) - Raw integer backward compatibility - Zero-value acceptance - Error paths: exceeds int max, negative value, invalid unit, non-numeric Document zero-value behavior in config.conf for all three maxMessageSize entries: "Setting to 0 rejects all non-empty request bodies". --- framework/src/main/resources/config.conf | 3 + .../org/tron/core/config/args/ArgsTest.java | 162 ++++++++++++++++++ 2 files changed, 165 insertions(+) diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index dcd399efa83..8fc779546c3 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -226,6 +226,7 @@ node { # The maximum request body size for HTTP API, default 4M (4194304 bytes). # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. + # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m } @@ -254,6 +255,7 @@ node { # The maximum message size allowed to be received on the server, default 4M (4194304 bytes). # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. + # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m # The maximum size of header list allowed to be received, default 8192 @@ -364,6 +366,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 non-negative. + # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m # Note: Before release_4.8.1, if you turn on jsonrpc and run it for a while and then turn it off, 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 1bed3e1c6ad..63878f21740 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 @@ -17,6 +17,7 @@ import com.google.common.collect.Lists; import com.typesafe.config.Config; +import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigFactory; import io.grpc.internal.GrpcUtil; import io.grpc.netty.NettyServerBuilder; @@ -39,6 +40,7 @@ import org.tron.common.utils.LocalWitnesses; import org.tron.common.utils.PublicMethod; import org.tron.core.config.Configuration; +import org.tron.core.exception.TronError; @Slf4j public class ArgsTest { @@ -362,4 +364,164 @@ public void testConfigStorageDefaults() { Args.clearParam(); } + + @Test + public void testMaxMessageSizeHumanReadable() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + + // --- KB tier: binary (k/K/Ki/KiB = 1024) vs SI (kB = 1000) --- + configMap.put("node.rpc.maxMessageSize", "512k"); + configMap.put("node.http.maxMessageSize", "512K"); + configMap.put("node.jsonrpc.maxMessageSize", "512kB"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(512 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(512 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(512 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + configMap.put("node.rpc.maxMessageSize", "256Ki"); + configMap.put("node.http.maxMessageSize", "256KiB"); + configMap.put("node.jsonrpc.maxMessageSize", "256kB"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(256 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(256 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(256 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + // --- MB tier: binary (m/M/Mi/MiB = 1024*1024) vs SI (MB = 1000*1000) --- + configMap.put("node.rpc.maxMessageSize", "4m"); + configMap.put("node.http.maxMessageSize", "8M"); + configMap.put("node.jsonrpc.maxMessageSize", "2MB"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(8 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(2 * 1000 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + configMap.put("node.rpc.maxMessageSize", "4Mi"); + configMap.put("node.http.maxMessageSize", "4MiB"); + configMap.put("node.jsonrpc.maxMessageSize", "4MB"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(4 * 1000 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + // --- GB tier: binary (g/G/Gi/GiB) vs SI (GB) --- + // rpc is int-bounded, only test http/jsonrpc for GB values + configMap.put("node.rpc.maxMessageSize", "4m"); + configMap.put("node.http.maxMessageSize", "1g"); + configMap.put("node.jsonrpc.maxMessageSize", "1GB"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(1024L * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(1000L * 1000 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + configMap.put("node.rpc.maxMessageSize", "4m"); + configMap.put("node.http.maxMessageSize", "2Gi"); + configMap.put("node.jsonrpc.maxMessageSize", "2GiB"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(2L * 1024 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(2L * 1024 * 1024 * 1024, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + // --- raw integer (backward compatible): treated as bytes --- + configMap.put("node.rpc.maxMessageSize", "4194304"); + configMap.put("node.http.maxMessageSize", "4194304"); + configMap.put("node.jsonrpc.maxMessageSize", "4194304"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + // --- zero is allowed --- + configMap.put("node.rpc.maxMessageSize", "0"); + configMap.put("node.http.maxMessageSize", "0"); + configMap.put("node.jsonrpc.maxMessageSize", "0"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(0, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(0, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(0, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + } + + @Test + public void testRpcMaxMessageSizeExceedsIntMax() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.rpc.maxMessageSize", "3g"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + try { + Args.applyConfigParams(config); + Assert.fail("expected TronError for rpc maxMessageSize > Integer.MAX_VALUE"); + } catch (TronError e) { + Assert.assertTrue(e.getMessage().contains("node.rpc.maxMessageSize must be non-negative")); + } + } + + @Test + public void testMaxMessageSizeNegativeValue() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.rpc.maxMessageSize", "-4m"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + try { + Args.applyConfigParams(config); + Assert.fail("expected IllegalArgumentException for negative memory size"); + } catch (IllegalArgumentException e) { + Assert.assertTrue(e.getMessage().contains("negative")); + } + } + + @Test + public void testMaxMessageSizeInvalidUnit() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.rpc.maxMessageSize", "4x"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + try { + Args.applyConfigParams(config); + Assert.fail("expected ConfigException.BadValue for invalid unit"); + } catch (ConfigException.BadValue e) { + Assert.assertTrue(e.getMessage().contains("Could not parse size-in-bytes unit")); + } + } + + @Test + public void testMaxMessageSizeNonNumeric() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.http.maxMessageSize", "abc"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + try { + Args.applyConfigParams(config); + Assert.fail("expected ConfigException.BadValue for non-numeric value"); + } catch (ConfigException.BadValue e) { + Assert.assertTrue(e.getMessage().contains("No number in size-in-bytes value")); + } + } } From aea0be80b578a1dd61648f3978590600192e8907 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 15 Apr 2026 15:04:40 +0800 Subject: [PATCH 16/19] test(http): add real JSON-RPC integration test and clean up SizeLimitHandler tests Add testJsonRpcSizeLimitIntegration() in JsonrpcServiceTest using the Spring-injected FullNodeJsonRpcHttpService (real JsonRpcServlet + jsonrpc4j) to verify SizeLimitHandler does not introduce regressions. Covers: normal request passthrough, Content-Length oversized 413, and chunked oversized behavior (200 empty body due to RateLimiterServlet absorbing BadMessageException). Clean up SizeLimitHandlerTest: remove 3 redundant testJsonRpcBody* tests that used BroadCatchServlet (cannot represent real jsonrpc4j chain), rename TestJsonRpcService to SecondHttpService, remove banner-style ruler comments, fix stale EchoServlet Javadoc reference, and remove HTML tags from Javadoc. --- .../common/jetty/SizeLimitHandlerTest.java | 99 ++++++------------- .../tron/core/jsonrpc/JsonrpcServiceTest.java | 74 ++++++++++++++ 2 files changed, 105 insertions(+), 68 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 7c264cc8fdb..d3f60f9b119 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -32,31 +32,28 @@ import org.tron.core.config.args.Args; /** - * Tests the {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size - * enforcement configured in {@link HttpService initContextHandler()}. + * Tests {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size + * enforcement configured in {@link HttpService#initContextHandler()}. * - *

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 {@code GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE} (4 MB).
  • - *
+ * Covers: accept/reject by size, UTF-8 byte counting, independent limits + * across HttpService instances, chunked transfer, and zero-limit behavior. + * + * Real JsonRpcServlet integration is tested separately in + * {@code JsonrpcServiceTest#testJsonRpcSizeLimitIntegration}. */ @Slf4j public class SizeLimitHandlerTest { private static final int HTTP_MAX_BODY_SIZE = 1024; - private static final int JSONRPC_MAX_BODY_SIZE = 512; + private static final int SECOND_SERVICE_MAX_BODY_SIZE = 512; @ClassRule public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); - private static TestHttpService httpService; - private static TestJsonRpcService jsonRpcService; - private static URI httpServerUri; - private static URI jsonRpcServerUri; + private static TestHttpService httpService; + private static SecondHttpService secondService; + private static URI httpServerUri; + private static URI secondServerUri; private static CloseableHttpClient client; /** @@ -96,9 +93,9 @@ protected void addServlet(ServletContextHandler context) { } } - /** Minimal concrete {@link HttpService} simulating a JSON-RPC service. */ - static class TestJsonRpcService extends HttpService { - TestJsonRpcService(int port, long maxRequestSize) { + /** Second HttpService instance with a different size limit, for independence tests. */ + static class SecondHttpService extends HttpService { + SecondHttpService(int port, long maxRequestSize) { this.port = port; this.contextPath = "/"; this.maxRequestSize = maxRequestSize; @@ -106,7 +103,7 @@ static class TestJsonRpcService extends HttpService { @Override protected void addServlet(ServletContextHandler context) { - context.addServlet(new ServletHolder(new BroadCatchServlet()), "/jsonrpc"); + context.addServlet(new ServletHolder(new BroadCatchServlet()), "/*"); } } @@ -115,17 +112,17 @@ public static void setup() throws Exception { 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); + Args.getInstance().setJsonRpcMaxMessageSize(SECOND_SERVICE_MAX_BODY_SIZE); 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)); + int secondPort = PublicMethod.chooseRandomPort(); + secondService = new SecondHttpService(secondPort, SECOND_SERVICE_MAX_BODY_SIZE); + secondService.start().get(10, TimeUnit.SECONDS); + secondServerUri = new URI(String.format("http://localhost:%d/", secondPort)); client = HttpClients.createDefault(); } @@ -142,16 +139,14 @@ public static void teardown() throws Exception { httpService.stop(); } } finally { - if (jsonRpcService != null) { - jsonRpcService.stop(); + if (secondService != null) { + secondService.stop(); } } Args.clearParam(); } } - // -- HTTP service body-size tests ------------------------------------------- - @Test public void testHttpBodyWithinLimit() throws Exception { Assert.assertEquals(200, post(httpServerUri, new StringEntity("small body"))); @@ -169,40 +164,16 @@ public void testHttpBodyAtExactLimit() throws Exception { post(httpServerUri, new StringEntity(repeat('b', HTTP_MAX_BODY_SIZE)))); } - // -- JSON-RPC service body-size tests --------------------------------------- - - @Test - public void testJsonRpcBodyWithinLimit() throws Exception { - Assert.assertEquals(200, - post(jsonRpcServerUri, new StringEntity("{\"method\":\"eth_blockNumber\"}"))); - } - - @Test - 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); + public void testTwoServicesHaveIndependentLimits() throws Exception { + // A body that exceeds secondService limit but is within httpService limit + String body = repeat('d', SECOND_SERVICE_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))); + Assert.assertEquals(413, post(secondServerUri, 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 @@ -212,10 +183,8 @@ 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). + * Chunked request within the limit should succeed. * InputStreamEntity with size=-1 sends chunked Transfer-Encoding (no Content-Length). */ @Test @@ -228,11 +197,11 @@ public void testChunkedBodyWithinLimit() throws Exception { /** * Chunked oversized body hitting a servlet with broad catch(Exception). * - *

SizeLimitHandler's LimitInterceptor throws BadMessageException during + * 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. + * OOM protection still works: the body read is truncated at the limit. */ @Test public void testChunkedBodyExceedsLimit() throws Exception { @@ -252,8 +221,6 @@ public void testChunkedBodyExceedsLimit() throws Exception { 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, @@ -278,8 +245,6 @@ 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) @@ -320,7 +285,7 @@ public void testWireBytesMatchCheckBodySizeForUtf8Json() throws Exception { /** * 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 — + * This makes {@code checkBodySize} measure fewer bytes than the wire — * a safe direction: checkBodySize never rejects what SizeLimitHandler accepts. */ @Test @@ -338,8 +303,6 @@ public void testCheckBodySizeSafeDirectionWithNewlines() throws Exception { 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); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index ced7048c9d2..dc48972203a 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -8,6 +8,8 @@ import com.google.gson.JsonObject; import com.google.protobuf.ByteString; import io.prometheus.client.CollectorRegistry; +import java.io.ByteArrayInputStream; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -15,6 +17,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.http.client.methods.CloseableHttpResponse; 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; @@ -25,6 +28,7 @@ import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.application.HttpService; import org.tron.common.parameter.CommonParameter; import org.tron.common.prometheus.Metrics; import org.tron.common.utils.ByteArray; @@ -1099,4 +1103,74 @@ public void testWeb3ClientVersion() { Assert.fail(); } } + + /** + * Verifies SizeLimitHandler integration with the real JsonRpcServlet + jsonrpc4j stack. + * + * Covers: normal request no regression, Content-Length oversized 413, + * and chunked oversized handled gracefully (body truncated, 200 + empty body + * because RateLimiterServlet absorbs the BadMessageException). + */ + @Test + public void testJsonRpcSizeLimitIntegration() { + long testLimit = 1024; + try { + Field field = HttpService.class.getDeclaredField("maxRequestSize"); + field.setAccessible(true); + long originalLimit = field.getLong(fullNodeJsonRpcHttpService); + field.setLong(fullNodeJsonRpcHttpService, testLimit); + + fullNodeJsonRpcHttpService.start(); + String url = "http://127.0.0.1:" + + CommonParameter.getInstance().getJsonRpcHttpFullNodePort() + "/jsonrpc"; + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + // Normal JSON-RPC request passes through SizeLimitHandler + JsonObject req = new JsonObject(); + req.addProperty("jsonrpc", "2.0"); + req.addProperty("method", "web3_clientVersion"); + req.addProperty("id", 1); + + HttpPost post = new HttpPost(url); + post.addHeader("Content-Type", "application/json"); + post.setEntity(new StringEntity(req.toString())); + CloseableHttpResponse resp = httpClient.execute(post); + Assert.assertEquals(200, resp.getStatusLine().getStatusCode()); + String body = EntityUtils.toString(resp.getEntity()); + Assert.assertTrue("Normal JSON-RPC response should contain result", + body.contains("result")); + resp.close(); + + // Oversized request with Content-Length → 413 before JsonRpcServlet + HttpPost overPost = new HttpPost(url); + overPost.addHeader("Content-Type", "application/json"); + overPost.setEntity(new StringEntity( + new String(new char[(int) testLimit + 1]).replace('\0', 'x'))); + resp = httpClient.execute(overPost); + Assert.assertEquals(413, resp.getStatusLine().getStatusCode()); + resp.close(); + + // Chunked oversized → BadMessageException thrown during body read, + // absorbed by RateLimiterServlet catch(Exception) → 200 with empty body. + // Body read IS truncated at the limit — OOM protection effective. + byte[] chunkedData = new String(new char[(int) testLimit * 2]) + .replace('\0', 'x').getBytes("UTF-8"); + HttpPost chunkedPost = new HttpPost(url); + chunkedPost.setEntity(new InputStreamEntity( + new ByteArrayInputStream(chunkedData), -1)); + resp = httpClient.execute(chunkedPost); + Assert.assertEquals(200, resp.getStatusLine().getStatusCode()); + body = EntityUtils.toString(resp.getEntity()); + Assert.assertTrue("Chunked oversized should return empty body" + + " (RateLimiterServlet absorbs BadMessageException)", body.isEmpty()); + resp.close(); + } finally { + field.setLong(fullNodeJsonRpcHttpService, originalLimit); + } + } catch (Exception e) { + Assert.fail(e.getMessage()); + } finally { + fullNodeJsonRpcHttpService.stop(); + } + } } From 58c258dcd2d001c11e72f95d9e577e8aada72122 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 16 Apr 2026 14:38:30 +0800 Subject: [PATCH 17/19] refactor(http): replace reflection with @VisibleForTesting accessor and fix comment attribution Add getMaxRequestSize()/setMaxRequestSize() to HttpService so tests use compile-safe accessors instead of Field.setAccessible(true). Correct comments attributing exception swallowing to RateLimiterServlet when it is actually jsonrpc4j that silently absorbs the BadMessageException. --- .../org/tron/common/application/HttpService.java | 11 +++++++++++ .../tron/core/jsonrpc/JsonrpcServiceTest.java | 16 +++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) 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 66a21a8a4ed..1318fd96527 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -15,6 +15,7 @@ package org.tron.common.application; +import com.google.common.annotations.VisibleForTesting; import java.util.concurrent.CompletableFuture; import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.server.ConnectionLimit; @@ -32,6 +33,16 @@ public abstract class HttpService extends AbstractService { protected long maxRequestSize = 4 * 1024 * 1024; // 4MB + @VisibleForTesting + public long getMaxRequestSize() { + return this.maxRequestSize; + } + + @VisibleForTesting + public void setMaxRequestSize(long maxRequestSize) { + this.maxRequestSize = maxRequestSize; + } + @Override public void innerStart() throws Exception { if (this.apiServer != null) { diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index dc48972203a..1cf5178202a 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -9,7 +9,7 @@ import com.google.protobuf.ByteString; import io.prometheus.client.CollectorRegistry; import java.io.ByteArrayInputStream; -import java.lang.reflect.Field; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -1109,16 +1109,14 @@ public void testWeb3ClientVersion() { * * Covers: normal request no regression, Content-Length oversized 413, * and chunked oversized handled gracefully (body truncated, 200 + empty body - * because RateLimiterServlet absorbs the BadMessageException). + * because jsonrpc4j absorbs the BadMessageException). */ @Test public void testJsonRpcSizeLimitIntegration() { long testLimit = 1024; try { - Field field = HttpService.class.getDeclaredField("maxRequestSize"); - field.setAccessible(true); - long originalLimit = field.getLong(fullNodeJsonRpcHttpService); - field.setLong(fullNodeJsonRpcHttpService, testLimit); + long originalLimit = fullNodeJsonRpcHttpService.getMaxRequestSize(); + fullNodeJsonRpcHttpService.setMaxRequestSize(testLimit); fullNodeJsonRpcHttpService.start(); String url = "http://127.0.0.1:" @@ -1151,7 +1149,7 @@ public void testJsonRpcSizeLimitIntegration() { resp.close(); // Chunked oversized → BadMessageException thrown during body read, - // absorbed by RateLimiterServlet catch(Exception) → 200 with empty body. + // absorbed by jsonrpc4j catch(Exception) → 200 with empty body. // Body read IS truncated at the limit — OOM protection effective. byte[] chunkedData = new String(new char[(int) testLimit * 2]) .replace('\0', 'x').getBytes("UTF-8"); @@ -1162,10 +1160,10 @@ public void testJsonRpcSizeLimitIntegration() { Assert.assertEquals(200, resp.getStatusLine().getStatusCode()); body = EntityUtils.toString(resp.getEntity()); Assert.assertTrue("Chunked oversized should return empty body" - + " (RateLimiterServlet absorbs BadMessageException)", body.isEmpty()); + + " (jsonrpc4j absorbs BadMessageException)", body.isEmpty()); resp.close(); } finally { - field.setLong(fullNodeJsonRpcHttpService, originalLimit); + fullNodeJsonRpcHttpService.setMaxRequestSize(originalLimit); } } catch (Exception e) { Assert.fail(e.getMessage()); From f36075040107ca25c244cf558e10e9eeee795db6 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 20 Apr 2026 10:05:07 +0800 Subject: [PATCH 18/19] test(http): address review comments - assertThrows, ASCII punctuation, finally guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace try/catch/fail pattern with Assert.assertThrows in UtilTest and ArgsTest (4 cases) - Replace non-ASCII punctuation (→, —) with ASCII (->, -) in newly added test comments - Hoist originalLimit before outer try in testJsonRpcSizeLimitIntegration so restore executes even when start() throws --- .../common/jetty/SizeLimitHandlerTest.java | 18 +++++----- .../org/tron/core/config/args/ArgsTest.java | 36 +++++++------------ .../tron/core/jsonrpc/JsonrpcServiceTest.java | 13 ++++--- .../org/tron/core/services/http/UtilTest.java | 9 ++--- 4 files changed, 30 insertions(+), 46 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 d3f60f9b119..685a861bc92 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -58,7 +58,7 @@ public class SizeLimitHandlerTest { /** * Simulates the real servlet pattern: reads body via getReader(), wraps in - * broad catch(Exception) — mirrors what RateLimiterServlet + actual servlets do. + * broad catch(Exception) - mirrors what RateLimiterServlet + actual servlets do. */ public static class BroadCatchServlet extends HttpServlet { @Override @@ -214,8 +214,8 @@ public void testChunkedBodyExceedsLimit() throws Exception { 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. + // 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")); @@ -223,8 +223,8 @@ public void testChunkedBodyExceedsLimit() throws Exception { /** * 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 + * 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 @@ -248,7 +248,7 @@ public void testZeroLimitRejectsAllBodies() throws Exception { /** * 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. + * must be identical - the two enforcement layers agree exactly. */ @Test public void testWireBytesMatchCheckBodySizeForAsciiJson() throws Exception { @@ -266,8 +266,8 @@ public void testWireBytesMatchCheckBodySizeForAsciiJson() throws Exception { /** * 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. + * {@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 { @@ -285,7 +285,7 @@ public void testWireBytesMatchCheckBodySizeForUtf8Json() throws Exception { /** * 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 — + * This makes {@code checkBodySize} measure fewer bytes than the wire - * a safe direction: checkBodySize never rejects what SizeLimitHandler accepts. */ @Test 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 63878f21740..6dc9f3d6ecb 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 @@ -472,12 +472,9 @@ public void testRpcMaxMessageSizeExceedsIntMax() { configMap.put("node.rpc.maxMessageSize", "3g"); Config config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(configMap)); - try { - Args.applyConfigParams(config); - Assert.fail("expected TronError for rpc maxMessageSize > Integer.MAX_VALUE"); - } catch (TronError e) { - Assert.assertTrue(e.getMessage().contains("node.rpc.maxMessageSize must be non-negative")); - } + TronError e = Assert.assertThrows(TronError.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue(e.getMessage().contains("node.rpc.maxMessageSize must be non-negative")); } @Test @@ -487,12 +484,9 @@ public void testMaxMessageSizeNegativeValue() { configMap.put("node.rpc.maxMessageSize", "-4m"); Config config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(configMap)); - try { - Args.applyConfigParams(config); - Assert.fail("expected IllegalArgumentException for negative memory size"); - } catch (IllegalArgumentException e) { - Assert.assertTrue(e.getMessage().contains("negative")); - } + IllegalArgumentException e = Assert.assertThrows(IllegalArgumentException.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue(e.getMessage().contains("negative")); } @Test @@ -502,12 +496,9 @@ public void testMaxMessageSizeInvalidUnit() { configMap.put("node.rpc.maxMessageSize", "4x"); Config config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(configMap)); - try { - Args.applyConfigParams(config); - Assert.fail("expected ConfigException.BadValue for invalid unit"); - } catch (ConfigException.BadValue e) { - Assert.assertTrue(e.getMessage().contains("Could not parse size-in-bytes unit")); - } + ConfigException.BadValue e = Assert.assertThrows(ConfigException.BadValue.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue(e.getMessage().contains("Could not parse size-in-bytes unit")); } @Test @@ -517,11 +508,8 @@ public void testMaxMessageSizeNonNumeric() { configMap.put("node.http.maxMessageSize", "abc"); Config config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(configMap)); - try { - Args.applyConfigParams(config); - Assert.fail("expected ConfigException.BadValue for non-numeric value"); - } catch (ConfigException.BadValue e) { - Assert.assertTrue(e.getMessage().contains("No number in size-in-bytes value")); - } + ConfigException.BadValue e = Assert.assertThrows(ConfigException.BadValue.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue(e.getMessage().contains("No number in size-in-bytes value")); } } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index 1cf5178202a..cac7fc85714 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -1114,8 +1114,8 @@ public void testWeb3ClientVersion() { @Test public void testJsonRpcSizeLimitIntegration() { long testLimit = 1024; + long originalLimit = fullNodeJsonRpcHttpService.getMaxRequestSize(); try { - long originalLimit = fullNodeJsonRpcHttpService.getMaxRequestSize(); fullNodeJsonRpcHttpService.setMaxRequestSize(testLimit); fullNodeJsonRpcHttpService.start(); @@ -1139,7 +1139,7 @@ public void testJsonRpcSizeLimitIntegration() { body.contains("result")); resp.close(); - // Oversized request with Content-Length → 413 before JsonRpcServlet + // Oversized request with Content-Length -> 413 before JsonRpcServlet HttpPost overPost = new HttpPost(url); overPost.addHeader("Content-Type", "application/json"); overPost.setEntity(new StringEntity( @@ -1148,9 +1148,9 @@ public void testJsonRpcSizeLimitIntegration() { Assert.assertEquals(413, resp.getStatusLine().getStatusCode()); resp.close(); - // Chunked oversized → BadMessageException thrown during body read, - // absorbed by jsonrpc4j catch(Exception) → 200 with empty body. - // Body read IS truncated at the limit — OOM protection effective. + // Chunked oversized -> BadMessageException thrown during body read, + // absorbed by jsonrpc4j catch(Exception) -> 200 with empty body. + // Body read IS truncated at the limit - OOM protection effective. byte[] chunkedData = new String(new char[(int) testLimit * 2]) .replace('\0', 'x').getBytes("UTF-8"); HttpPost chunkedPost = new HttpPost(url); @@ -1162,12 +1162,11 @@ public void testJsonRpcSizeLimitIntegration() { Assert.assertTrue("Chunked oversized should return empty body" + " (jsonrpc4j absorbs BadMessageException)", body.isEmpty()); resp.close(); - } finally { - fullNodeJsonRpcHttpService.setMaxRequestSize(originalLimit); } } catch (Exception e) { Assert.fail(e.getMessage()); } finally { + fullNodeJsonRpcHttpService.setMaxRequestSize(originalLimit); fullNodeJsonRpcHttpService.stop(); } } 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 24a8a2bdef3..ebcb530bca3 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 @@ -143,12 +143,9 @@ public void testCheckBodySizeUsesHttpLimit() throws Exception { 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")); - } + Exception e = Assert.assertThrows(Exception.class, + () -> Util.checkBodySize(exceedsHttpLimit)); + Assert.assertTrue(e.getMessage().contains("200")); } finally { Args.getInstance().setHttpMaxMessageSize(originalHttpMax); Args.getInstance().setMaxMessageSize(originalRpcMax); From bb14edd48adca18c883dbd04199854797a400d9c Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 22 Apr 2026 15:19:22 +0800 Subject: [PATCH 19/19] fix(config): cap http/jsonrpc maxMessageSize at Integer.MAX_VALUE Align node.http.maxMessageSize and node.jsonrpc.maxMessageSize with the existing node.rpc.maxMessageSize rule: reject values > Integer.MAX_VALUE at startup with TronError. Downstream body materialization is int-bounded (String/byte[]/StringBuilder all cap at Integer.MAX_VALUE), so config values beyond that produced an ambiguous silent mismatch; fail-fast is safer than a runtime NegativeArraySizeException or truncation surprise. Update config.conf docblocks (http/rpc/jsonrpc) to state the full constraint. Replace the obsolete 2Gi success assertion in testMaxMessageSizeHumanReadable, add testHttpMaxMessageSizeExceedsIntMax and testJsonRpcMaxMessageSizeExceedsIntMax mirroring the existing rpc case. --- .../java/org/tron/core/config/args/Args.java | 12 ++++-- framework/src/main/resources/config.conf | 9 +++-- .../org/tron/core/config/args/ArgsTest.java | 38 +++++++++++++------ 3 files changed, 40 insertions(+), 19 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 ff20ed30241..3e181e118db 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 @@ -536,15 +536,19 @@ public static void applyConfigParams( PARAMETER.httpMaxMessageSize = config.hasPath(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE).toBytes() : defaultMaxMessageSize; - if (PARAMETER.httpMaxMessageSize < 0) { - throw new TronError("node.http.maxMessageSize must be non-negative, got: " + if (PARAMETER.httpMaxMessageSize < 0 + || PARAMETER.httpMaxMessageSize > Integer.MAX_VALUE) { + throw new TronError("node.http.maxMessageSize must be non-negative and <= " + + Integer.MAX_VALUE + ", got: " + PARAMETER.httpMaxMessageSize, PARAMETER_INIT); } PARAMETER.jsonRpcMaxMessageSize = config.hasPath(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE).toBytes() : defaultMaxMessageSize; - if (PARAMETER.jsonRpcMaxMessageSize < 0) { - throw new TronError("node.jsonrpc.maxMessageSize must be non-negative, got: " + if (PARAMETER.jsonRpcMaxMessageSize < 0 + || PARAMETER.jsonRpcMaxMessageSize > Integer.MAX_VALUE) { + throw new TronError("node.jsonrpc.maxMessageSize must be non-negative and <= " + + Integer.MAX_VALUE + ", got: " + PARAMETER.jsonRpcMaxMessageSize, PARAMETER_INIT); } diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 8fc779546c3..e5ea11dd90e 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -225,7 +225,8 @@ node { PBFTPort = 8092 # The maximum request body size for HTTP API, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. + # Supports human-readable sizes: 4m, 4MB, 4194304. + # Must be non-negative and <= 2147483647 (Integer.MAX_VALUE, ~2 GiB). # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m } @@ -254,7 +255,8 @@ 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 non-negative. + # Supports human-readable sizes: 4m, 4MB, 4194304. + # Must be non-negative and <= 2147483647 (Integer.MAX_VALUE, ~2 GiB). # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m @@ -365,7 +367,8 @@ node { jsonrpc { # The maximum request body size for JSON-RPC API, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. + # Supports human-readable sizes: 4m, 4MB, 4194304. + # Must be non-negative and <= 2147483647 (Integer.MAX_VALUE, ~2 GiB). # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m 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 6dc9f3d6ecb..f13bf4c4d22 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 @@ -417,7 +417,7 @@ public void testMaxMessageSizeHumanReadable() { Args.clearParam(); // --- GB tier: binary (g/G/Gi/GiB) vs SI (GB) --- - // rpc is int-bounded, only test http/jsonrpc for GB values + // All three paths are int-bounded; values up to Integer.MAX_VALUE are accepted. configMap.put("node.rpc.maxMessageSize", "4m"); configMap.put("node.http.maxMessageSize", "1g"); configMap.put("node.jsonrpc.maxMessageSize", "1GB"); @@ -429,17 +429,6 @@ public void testMaxMessageSizeHumanReadable() { Assert.assertEquals(1000L * 1000 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); Args.clearParam(); - configMap.put("node.rpc.maxMessageSize", "4m"); - configMap.put("node.http.maxMessageSize", "2Gi"); - configMap.put("node.jsonrpc.maxMessageSize", "2GiB"); - config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)); - Args.applyConfigParams(config); - Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); - Assert.assertEquals(2L * 1024 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); - Assert.assertEquals(2L * 1024 * 1024 * 1024, Args.getInstance().getJsonRpcMaxMessageSize()); - Args.clearParam(); - // --- raw integer (backward compatible): treated as bytes --- configMap.put("node.rpc.maxMessageSize", "4194304"); configMap.put("node.http.maxMessageSize", "4194304"); @@ -477,6 +466,31 @@ public void testRpcMaxMessageSizeExceedsIntMax() { Assert.assertTrue(e.getMessage().contains("node.rpc.maxMessageSize must be non-negative")); } + @Test + public void testHttpMaxMessageSizeExceedsIntMax() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.http.maxMessageSize", "2Gi"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + TronError e = Assert.assertThrows(TronError.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue(e.getMessage().contains("node.http.maxMessageSize must be non-negative")); + } + + @Test + public void testJsonRpcMaxMessageSizeExceedsIntMax() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.jsonrpc.maxMessageSize", "2Gi"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + TronError e = Assert.assertThrows(TronError.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue( + e.getMessage().contains("node.jsonrpc.maxMessageSize must be non-negative")); + } + @Test public void testMaxMessageSizeNegativeValue() { Map configMap = new HashMap<>();