From b32fa5887f89d314e5c0bd787ee9195723436369 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Wed, 18 Mar 2026 22:46:15 +0800 Subject: [PATCH 01/13] restrict batch size and response size of jsonrpc --- .../common/parameter/CommonParameter.java | 6 ++ .../org/tron/core/config/args/NodeConfig.java | 2 + common/src/main/resources/reference.conf | 6 ++ .../java/org/tron/core/config/args/Args.java | 2 + .../filter/BufferedResponseWrapper.java | 61 ++++++++++++++++ .../filter/CachedBodyRequestWrapper.java | 50 ++++++++++++++ .../core/services/jsonrpc/JsonRpcServlet.java | 69 +++++++++++++++++-- framework/src/main/resources/config.conf | 4 ++ 8 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java create mode 100644 framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java 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..29f2b909d66 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -459,6 +459,12 @@ public class CommonParameter { @Getter @Setter public int jsonRpcMaxBlockFilterNum = 50000; + @Getter + @Setter + public int jsonRpcMaxBatchSize = 1; + @Getter + @Setter + public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index c3305e976de..c28b37e871c 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -302,6 +302,8 @@ public void setHttpPBFTPort(int v) { private int maxBlockRange = 5000; private int maxSubTopics = 1000; private int maxBlockFilterNum = 50000; + private int maxBatchSize = 100; + private int maxResponseSize = 25 * 1024 * 1024; } @Getter diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 11970a0a673..ed5ef5684b3 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -400,6 +400,12 @@ node { # Maximum number for blockFilter maxBlockFilterNum = 50000 + + # Maximum number of requests in a JSON-RPC batch, >0 otherwise no limit + maxBatchSize = 100 + + # Maximum response body size in bytes for JSON-RPC (default 25MB) + maxResponseSize = 26214400 } # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. 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 f91c6a437ac..f13ca7f7303 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 @@ -585,6 +585,8 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.jsonRpcMaxBlockRange = jsonrpc.getMaxBlockRange(); PARAMETER.jsonRpcMaxSubTopics = jsonrpc.getMaxSubTopics(); PARAMETER.jsonRpcMaxBlockFilterNum = jsonrpc.getMaxBlockFilterNum(); + PARAMETER.jsonRpcMaxBatchSize = jsonrpc.getMaxBatchSize(); + PARAMETER.jsonRpcMaxResponseSize = jsonrpc.getMaxResponseSize(); // ---- P2P sub-bean ---- PARAMETER.nodeP2pVersion = nc.getP2p().getVersion(); diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java new file mode 100644 index 00000000000..2e4d8eb0ef2 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java @@ -0,0 +1,61 @@ +package org.tron.core.services.filter; + +import java.io.ByteArrayOutputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +/** + * Buffers the response body without writing to the underlying response, + * so the caller can inspect the size before committing. + */ +public class BufferedResponseWrapper extends HttpServletResponseWrapper { + + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private final ServletOutputStream outputStream = new ServletOutputStream() { + @Override + public void write(int b) { + buffer.write(b); + } + + @Override + public void write(byte[] b, int off, int len) { + buffer.write(b, off, len); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + } + }; + + public BufferedResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public ServletOutputStream getOutputStream() { + return outputStream; + } + + /** + * Suppress forwarding Content-Length to the real response; caller sets it after size check. + */ + @Override + public void setContentLength(int len) { + } + + @Override + public void setContentLengthLong(long len) { + } + + public byte[] toByteArray() { + return buffer.toByteArray(); + } + +} diff --git a/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java new file mode 100644 index 00000000000..efee8d9574e --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java @@ -0,0 +1,50 @@ +package org.tron.core.services.filter; + +import java.io.ByteArrayInputStream; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +/** + * Wraps a request and replays a pre-read body from a byte array. + */ +public class CachedBodyRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + public CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) { + super(request); + this.body = body; + } + + @Override + public ServletInputStream getInputStream() { + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + return new ServletInputStream() { + @Override + public int read() { + return bais.read(); + } + + @Override + public int read(byte[] b, int off, int len) { + return bais.read(b, off, len); + } + + @Override + public boolean isFinished() { + return bais.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + }; + } +} diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index 104a0e9e470..fb26c400a24 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -1,10 +1,15 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.googlecode.jsonrpc4j.HttpStatusCodeProvider; import com.googlecode.jsonrpc4j.JsonRpcInterceptor; import com.googlecode.jsonrpc4j.JsonRpcServer; import com.googlecode.jsonrpc4j.ProxyUtil; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Collections; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -14,15 +19,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.parameter.CommonParameter; -import org.tron.core.Wallet; -import org.tron.core.db.Manager; -import org.tron.core.services.NodeInfoService; +import org.tron.core.services.filter.BufferedResponseWrapper; +import org.tron.core.services.filter.CachedBodyRequestWrapper; import org.tron.core.services.http.RateLimiterServlet; @Component @Slf4j(topic = "API") public class JsonRpcServlet extends RateLimiterServlet { + private static final ObjectMapper MAPPER = new ObjectMapper(); + private JsonRpcServer rpcServer = null; @Autowired @@ -66,6 +72,59 @@ public Integer getJsonRpcCode(int httpStatusCode) { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { - rpcServer.handle(req, resp); + CommonParameter parameter = CommonParameter.getInstance(); + + // Read request body so we can inspect and replay it + byte[] body = readBody(req.getInputStream()); + + // Check batch request array length + JsonNode rootNode = MAPPER.readTree(body); + if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) { + writeJsonRpcError(resp, + "Batch size " + rootNode.size() + " exceeds the limit of " + + parameter.getJsonRpcMaxBatchSize(), null); + return; + } + + // Buffer the response to check its size before committing + BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(resp); + rpcServer.handle(new CachedBodyRequestWrapper(req, body), bufferedResp); + + byte[] responseBytes = bufferedResp.toByteArray(); + logger.info("responseBytes: {}", responseBytes.length); + if (responseBytes.length > parameter.getJsonRpcMaxResponseSize()) { + JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null; + writeJsonRpcError(resp, + "Response byte size " + responseBytes.length + " exceeds the limit of " + + parameter.getJsonRpcMaxResponseSize(), idNode); + return; + } + + resp.setContentLength(responseBytes.length); + resp.getOutputStream().write(responseBytes); + resp.getOutputStream().flush(); + } + + private byte[] readBody(InputStream in) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] tmp = new byte[4096]; + int n; + while ((n = in.read(tmp)) != -1) { + buffer.write(tmp, 0, n); + } + return buffer.toByteArray(); + } + + private void writeJsonRpcError(HttpServletResponse resp, String message, JsonNode id) + throws IOException { + String idStr = (id != null && !id.isNull() && !id.isMissingNode()) ? id.toString() : "null"; + String body = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":" + -32005 + + ",\"message\":\"" + message + "\"},\"id\":" + idStr + "}"; + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + resp.setContentType("application/json"); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentLength(bytes.length); + resp.getOutputStream().write(bytes); + resp.getOutputStream().flush(); } -} \ No newline at end of file +} diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 369924074bc..41e1bbe43e6 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -375,6 +375,10 @@ node { maxSubTopics = 1000 # Allowed maximum number for blockFilter maxBlockFilterNum = 50000 + # Allowed batch size + maxBatchSize = 1 + # Allowed max response byte size + maxResponseSize = 25 * 1024 * 1024 } # Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode, From 02a588f38a5b692a522d9bf51e5067a42060ac85 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Wed, 1 Apr 2026 20:52:12 +0800 Subject: [PATCH 02/13] add node.jsonrpc.maxAddressSize and node.jsonrpc.maxRequestTimeout --- .../common/parameter/CommonParameter.java | 6 ++ .../org/tron/core/config/args/NodeConfig.java | 2 + .../JsonRpcResponseTooLargeException.java | 17 +++++ common/src/main/resources/reference.conf | 6 ++ .../java/org/tron/core/config/args/Args.java | 2 + .../filter/BufferedResponseWrapper.java | 22 +++++- .../core/services/jsonrpc/JsonRpcServlet.java | 73 +++++++++++++++---- .../services/jsonrpc/filters/LogFilter.java | 4 + framework/src/main/resources/config.conf | 7 +- 9 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java 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 29f2b909d66..9fcf8debd66 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -465,6 +465,12 @@ public class CommonParameter { @Getter @Setter public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; + @Getter + @Setter + public int jsonRpcMaxRequestTimeout = 30; + @Getter + @Setter + public int jsonRpcMaxAddressSize = 1000; @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index c28b37e871c..ab6315c1bbd 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -304,6 +304,8 @@ public void setHttpPBFTPort(int v) { private int maxBlockFilterNum = 50000; private int maxBatchSize = 100; private int maxResponseSize = 25 * 1024 * 1024; + private int maxRequestTimeout = 30; + private int maxAddressSize = 1000; } @Getter diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java new file mode 100644 index 00000000000..65c7bc28cf8 --- /dev/null +++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java @@ -0,0 +1,17 @@ +package org.tron.core.exception.jsonrpc; + +public class JsonRpcResponseTooLargeException extends RuntimeException { + + public JsonRpcResponseTooLargeException() { + super(); + } + + public JsonRpcResponseTooLargeException(String message) { + super(message); + } + + public JsonRpcResponseTooLargeException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index ed5ef5684b3..2764f37e074 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -406,6 +406,12 @@ node { # Maximum response body size in bytes for JSON-RPC (default 25MB) maxResponseSize = 26214400 + + # Maximum request timeout in seconds for JSON-RPC + maxRequestTimeout = 30 + + # Maximum number of addresses in a single JSON-RPC request + maxAddressSize = 1000 } # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. 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 f13ca7f7303..e1c57dc7a94 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 @@ -587,6 +587,8 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.jsonRpcMaxBlockFilterNum = jsonrpc.getMaxBlockFilterNum(); PARAMETER.jsonRpcMaxBatchSize = jsonrpc.getMaxBatchSize(); PARAMETER.jsonRpcMaxResponseSize = jsonrpc.getMaxResponseSize(); + PARAMETER.jsonRpcMaxRequestTimeout = jsonrpc.getMaxRequestTimeout(); + PARAMETER.jsonRpcMaxAddressSize = jsonrpc.getMaxAddressSize(); // ---- P2P sub-bean ---- PARAMETER.nodeP2pVersion = nc.getP2p().getVersion(); diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java index 2e4d8eb0ef2..fefea565c3c 100644 --- a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java +++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java @@ -5,22 +5,30 @@ import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; +import org.tron.core.exception.jsonrpc.JsonRpcResponseTooLargeException; /** * Buffers the response body without writing to the underlying response, * so the caller can inspect the size before committing. + * + *
If {@code maxBytes > 0}, writes that would push the buffer past {@code maxBytes} throw
+ * {@link JsonRpcResponseTooLargeException} immediately, bounding memory usage to at most
+ * {@code maxBytes} rather than the full response size.
*/
public class BufferedResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ private final int maxBytes;
private final ServletOutputStream outputStream = new ServletOutputStream() {
@Override
public void write(int b) {
+ checkLimit(1);
buffer.write(b);
}
@Override
public void write(byte[] b, int off, int len) {
+ checkLimit(len);
buffer.write(b, off, len);
}
@@ -34,8 +42,20 @@ public void setWriteListener(WriteListener writeListener) {
}
};
- public BufferedResponseWrapper(HttpServletResponse response) {
+ /**
+ * @param response the wrapped response
+ * @param maxBytes max allowed response bytes; {@code 0} means no limit
+ */
+ public BufferedResponseWrapper(HttpServletResponse response, int maxBytes) {
super(response);
+ this.maxBytes = maxBytes;
+ }
+
+ private void checkLimit(int incoming) {
+ if (maxBytes > 0 && buffer.size() + incoming > maxBytes) {
+ throw new JsonRpcResponseTooLargeException(
+ "Response byte size exceeds the limit of " + maxBytes);
+ }
}
@Override
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
index fb26c400a24..f7c830cbd45 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
@@ -2,6 +2,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.googlecode.jsonrpc4j.HttpStatusCodeProvider;
import com.googlecode.jsonrpc4j.JsonRpcInterceptor;
import com.googlecode.jsonrpc4j.JsonRpcServer;
@@ -11,6 +12,12 @@
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@@ -19,6 +26,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.tron.common.parameter.CommonParameter;
+import org.tron.core.exception.jsonrpc.JsonRpcResponseTooLargeException;
import org.tron.core.services.filter.BufferedResponseWrapper;
import org.tron.core.services.filter.CachedBodyRequestWrapper;
import org.tron.core.services.http.RateLimiterServlet;
@@ -29,6 +37,21 @@ public class JsonRpcServlet extends RateLimiterServlet {
private static final ObjectMapper MAPPER = new ObjectMapper();
+ private static final ExecutorService RPC_EXECUTOR = Executors.newCachedThreadPool(
+ new ThreadFactoryBuilder().setNameFormat("jsonrpc-timeout-%d").setDaemon(true).build());
+
+ enum JsonRpcError {
+ EXCEED_LIMIT(-32005),
+ RESPONSE_TOO_LARGE(-32003),
+ TIMEOUT(-32002);
+
+ final int code;
+
+ JsonRpcError(int code) {
+ this.code = code;
+ }
+ }
+
private JsonRpcServer rpcServer = null;
@Autowired
@@ -80,26 +103,50 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
// Check batch request array length
JsonNode rootNode = MAPPER.readTree(body);
if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) {
- writeJsonRpcError(resp,
+ writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT,
"Batch size " + rootNode.size() + " exceeds the limit of "
+ parameter.getJsonRpcMaxBatchSize(), null);
return;
}
- // Buffer the response to check its size before committing
- BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(resp);
- rpcServer.handle(new CachedBodyRequestWrapper(req, body), bufferedResp);
+ // Buffer the response; limit is enforced eagerly during writes to bound memory usage
+ int maxResponseSize = parameter.getJsonRpcMaxResponseSize();
+ CachedBodyRequestWrapper cachedReq = new CachedBodyRequestWrapper(req, body);
+ BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(resp, maxResponseSize);
+
+ int timeoutSec = parameter.getJsonRpcMaxRequestTimeout();
+ Future> future = RPC_EXECUTOR.submit(() -> {
+ try {
+ rpcServer.handle(cachedReq, bufferedResp);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
- byte[] responseBytes = bufferedResp.toByteArray();
- logger.info("responseBytes: {}", responseBytes.length);
- if (responseBytes.length > parameter.getJsonRpcMaxResponseSize()) {
+ try {
+ future.get(timeoutSec, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ future.cancel(true);
JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null;
- writeJsonRpcError(resp,
- "Response byte size " + responseBytes.length + " exceeds the limit of "
- + parameter.getJsonRpcMaxResponseSize(), idNode);
+ writeJsonRpcError(resp, JsonRpcError.TIMEOUT, "Request timeout after " + timeoutSec + "s",
+ idNode);
return;
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof RuntimeException
+ && cause.getCause() instanceof JsonRpcResponseTooLargeException) {
+ JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null;
+ writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, cause.getCause().getMessage(),
+ idNode);
+ return;
+ }
+ throw new IOException("RPC execution failed", cause);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IOException("RPC interrupted", e);
}
+ byte[] responseBytes = bufferedResp.toByteArray();
resp.setContentLength(responseBytes.length);
resp.getOutputStream().write(responseBytes);
resp.getOutputStream().flush();
@@ -115,10 +162,10 @@ private byte[] readBody(InputStream in) throws IOException {
return buffer.toByteArray();
}
- private void writeJsonRpcError(HttpServletResponse resp, String message, JsonNode id)
- throws IOException {
+ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, String message,
+ JsonNode id) throws IOException {
String idStr = (id != null && !id.isNull() && !id.isMissingNode()) ? id.toString() : "null";
- String body = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":" + -32005
+ String body = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":" + error.code
+ ",\"message\":\"" + message + "\"},\"id\":" + idStr + "}";
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
resp.setContentType("application/json");
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java
index 42bc123d4bc..d2bd58f6c56 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java
@@ -50,6 +50,10 @@ public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException {
withContractAddress(addressToByteArray((String) fr.getAddress()));
} else if (fr.getAddress() instanceof ArrayList) {
+ int maxAddressSize = Args.getInstance().getJsonRpcMaxAddressSize();
+ if (maxAddressSize > 0 && ((ArrayList>) fr.getAddress()).size() > maxAddressSize) {
+ throw new JsonRpcInvalidParamsException("exceed max addresses: " + maxAddressSize);
+ }
List If {@code maxBytes > 0}, writes that would push the buffer past {@code maxBytes} throw
* {@link JsonRpcResponseTooLargeException} immediately, bounding memory usage to at most
* {@code maxBytes} rather than the full response size.
+ *
+ * Header-mutating methods ({@code setStatus}, {@code setContentType}) are buffered here and
+ * only forwarded to the real response via {@link #commitToResponse()}, preventing a timed-out
+ * handler thread from racing with the timeout error writer.
*/
public class BufferedResponseWrapper extends HttpServletResponseWrapper {
+ private final HttpServletResponse actual;
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
private final int maxBytes;
+ private int status = HttpServletResponse.SC_OK;
+ private String contentType;
private final ServletOutputStream outputStream = new ServletOutputStream() {
@Override
public void write(int b) {
@@ -48,6 +56,7 @@ public void setWriteListener(WriteListener writeListener) {
*/
public BufferedResponseWrapper(HttpServletResponse response, int maxBytes) {
super(response);
+ this.actual = response;
this.maxBytes = maxBytes;
}
@@ -58,6 +67,16 @@ private void checkLimit(int incoming) {
}
}
+ @Override
+ public void setStatus(int sc) {
+ this.status = sc;
+ }
+
+ @Override
+ public void setContentType(String type) {
+ this.contentType = type;
+ }
+
@Override
public ServletOutputStream getOutputStream() {
return outputStream;
@@ -74,8 +93,14 @@ public void setContentLength(int len) {
public void setContentLengthLong(long len) {
}
- public byte[] toByteArray() {
- return buffer.toByteArray();
+ public void commitToResponse() throws IOException {
+ if (contentType != null) {
+ actual.setContentType(contentType);
+ }
+ actual.setStatus(status);
+ byte[] bytes = buffer.toByteArray();
+ actual.setContentLength(bytes.length);
+ actual.getOutputStream().write(bytes);
+ actual.getOutputStream().flush();
}
-
-}
+}
\ No newline at end of file
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
index f7c830cbd45..752528ab993 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
@@ -2,7 +2,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import com.googlecode.jsonrpc4j.HttpStatusCodeProvider;
import com.googlecode.jsonrpc4j.JsonRpcInterceptor;
import com.googlecode.jsonrpc4j.JsonRpcServer;
@@ -10,14 +10,7 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
import java.util.Collections;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@@ -37,13 +30,9 @@ public class JsonRpcServlet extends RateLimiterServlet {
private static final ObjectMapper MAPPER = new ObjectMapper();
- private static final ExecutorService RPC_EXECUTOR = Executors.newCachedThreadPool(
- new ThreadFactoryBuilder().setNameFormat("jsonrpc-timeout-%d").setDaemon(true).build());
-
enum JsonRpcError {
EXCEED_LIMIT(-32005),
- RESPONSE_TOO_LARGE(-32003),
- TIMEOUT(-32002);
+ RESPONSE_TOO_LARGE(-32003);
final int code;
@@ -97,10 +86,8 @@ public Integer getJsonRpcCode(int httpStatusCode) {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
CommonParameter parameter = CommonParameter.getInstance();
- // Read request body so we can inspect and replay it
- byte[] body = readBody(req.getInputStream());
+ byte[] body = readBody(req.getInputStream(), parameter.getJsonRpcMaxResponseSize());
- // Check batch request array length
JsonNode rootNode = MAPPER.readTree(body);
if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) {
writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT,
@@ -109,54 +96,33 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
return;
}
- // Buffer the response; limit is enforced eagerly during writes to bound memory usage
- int maxResponseSize = parameter.getJsonRpcMaxResponseSize();
CachedBodyRequestWrapper cachedReq = new CachedBodyRequestWrapper(req, body);
- BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(resp, maxResponseSize);
-
- int timeoutSec = parameter.getJsonRpcMaxRequestTimeout();
- Future> future = RPC_EXECUTOR.submit(() -> {
- try {
- rpcServer.handle(cachedReq, bufferedResp);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- });
+ BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(
+ resp, parameter.getJsonRpcMaxResponseSize());
try {
- future.get(timeoutSec, TimeUnit.SECONDS);
- } catch (TimeoutException e) {
- future.cancel(true);
+ rpcServer.handle(cachedReq, bufferedResp);
+ } catch (JsonRpcResponseTooLargeException e) {
JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null;
- writeJsonRpcError(resp, JsonRpcError.TIMEOUT, "Request timeout after " + timeoutSec + "s",
- idNode);
+ writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, e.getMessage(), idNode);
return;
- } catch (ExecutionException e) {
- Throwable cause = e.getCause();
- if (cause instanceof RuntimeException
- && cause.getCause() instanceof JsonRpcResponseTooLargeException) {
- JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null;
- writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, cause.getCause().getMessage(),
- idNode);
- return;
- }
- throw new IOException("RPC execution failed", cause);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new IOException("RPC interrupted", e);
+ } catch (Exception e) {
+ throw new IOException("RPC execution failed", e);
}
- byte[] responseBytes = bufferedResp.toByteArray();
- resp.setContentLength(responseBytes.length);
- resp.getOutputStream().write(responseBytes);
- resp.getOutputStream().flush();
+ bufferedResp.commitToResponse();
}
- private byte[] readBody(InputStream in) throws IOException {
+ private byte[] readBody(InputStream in, int maxBytes) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] tmp = new byte[4096];
+ int total = 0;
int n;
while ((n = in.read(tmp)) != -1) {
+ total += n;
+ if (maxBytes > 0 && total > maxBytes) {
+ throw new IOException("Request body exceeds maximum size of " + maxBytes + " bytes");
+ }
buffer.write(tmp, 0, n);
}
return buffer.toByteArray();
@@ -164,10 +130,17 @@ private byte[] readBody(InputStream in) throws IOException {
private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, String message,
JsonNode id) throws IOException {
- String idStr = (id != null && !id.isNull() && !id.isMissingNode()) ? id.toString() : "null";
- String body = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":" + error.code
- + ",\"message\":\"" + message + "\"},\"id\":" + idStr + "}";
- byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+ ObjectNode root = MAPPER.createObjectNode();
+ root.put("jsonrpc", "2.0");
+ ObjectNode errNode = root.putObject("error");
+ errNode.put("code", error.code);
+ errNode.put("message", message);
+ if (id != null && !id.isNull() && !id.isMissingNode()) {
+ root.set("id", id);
+ } else {
+ root.putNull("id");
+ }
+ byte[] bytes = MAPPER.writeValueAsBytes(root);
resp.setContentType("application/json");
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentLength(bytes.length);
diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf
index a615361d5f8..a804ff877ac 100644
--- a/framework/src/main/resources/config.conf
+++ b/framework/src/main/resources/config.conf
@@ -381,8 +381,6 @@ node {
maxBatchSize = 10
# Allowed max response byte size
maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B
- # Allowed max request processing time in seconds
- maxRequestTimeout = 30
}
# Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode,
From acd51cbca3b28cb3ad9b672956a44e5bb97cd501 Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Mon, 20 Apr 2026 11:19:39 +0800
Subject: [PATCH 06/13] remove input restrict
---
.../org/tron/core/services/jsonrpc/JsonRpcServlet.java | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
index 752528ab993..c66e3692b0c 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
@@ -86,7 +86,7 @@ public Integer getJsonRpcCode(int httpStatusCode) {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
CommonParameter parameter = CommonParameter.getInstance();
- byte[] body = readBody(req.getInputStream(), parameter.getJsonRpcMaxResponseSize());
+ byte[] body = readBody(req.getInputStream());
JsonNode rootNode = MAPPER.readTree(body);
if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) {
@@ -113,16 +113,11 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
bufferedResp.commitToResponse();
}
- private byte[] readBody(InputStream in, int maxBytes) throws IOException {
+ private byte[] readBody(InputStream in) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] tmp = new byte[4096];
- int total = 0;
int n;
while ((n = in.read(tmp)) != -1) {
- total += n;
- if (maxBytes > 0 && total > maxBytes) {
- throw new IOException("Request body exceeds maximum size of " + maxBytes + " bytes");
- }
buffer.write(tmp, 0, n);
}
return buffer.toByteArray();
From fa87c7efed9c1674d41c9e9d7f8cfaaac69c0fa6 Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Mon, 20 Apr 2026 11:40:08 +0800
Subject: [PATCH 07/13] optimize ContentType when writeJsonRpcError
---
.../java/org/tron/core/services/jsonrpc/JsonRpcServlet.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
index c66e3692b0c..d239bd708c3 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
@@ -136,7 +136,7 @@ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, Str
root.putNull("id");
}
byte[] bytes = MAPPER.writeValueAsBytes(root);
- resp.setContentType("application/json");
+ resp.setContentType("application/json; charset=utf-8");
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentLength(bytes.length);
resp.getOutputStream().write(bytes);
From 01da427d321aa6737d5188ad43829ce1b85878f4 Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Thu, 23 Apr 2026 17:26:22 +0800
Subject: [PATCH 08/13] add error code -32700
---
.../tron/core/services/jsonrpc/JsonRpcServlet.java | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
index d239bd708c3..a78eec974f1 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
@@ -31,6 +31,7 @@ public class JsonRpcServlet extends RateLimiterServlet {
private static final ObjectMapper MAPPER = new ObjectMapper();
enum JsonRpcError {
+ PARSE_ERROR(-32700),
EXCEED_LIMIT(-32005),
RESPONSE_TOO_LARGE(-32003);
@@ -86,9 +87,15 @@ public Integer getJsonRpcCode(int httpStatusCode) {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
CommonParameter parameter = CommonParameter.getInstance();
- byte[] body = readBody(req.getInputStream());
-
- JsonNode rootNode = MAPPER.readTree(body);
+ byte[] body;
+ JsonNode rootNode;
+ try {
+ body = readBody(req.getInputStream());
+ rootNode = MAPPER.readTree(body);
+ } catch (IOException e) {
+ writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse json error", null);
+ return;
+ }
if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) {
writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT,
"Batch size " + rootNode.size() + " exceeds the limit of "
From 7b2585d7a32ea510031e781b905a509285de5a5d Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Thu, 23 Apr 2026 17:42:55 +0800
Subject: [PATCH 09/13] add getReader for CachedBodyRequestWrapper; set
jsonRpcMaxBatchSize default as 100
---
.../org/tron/common/parameter/CommonParameter.java | 2 +-
.../core/services/filter/BufferedResponseWrapper.java | 2 +-
.../services/filter/CachedBodyRequestWrapper.java | 11 +++++++++++
framework/src/main/resources/config.conf | 2 +-
4 files changed, 14 insertions(+), 3 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 db14ba84788..01ded57ddc9 100644
--- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java
+++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java
@@ -461,7 +461,7 @@ public class CommonParameter {
public int jsonRpcMaxBlockFilterNum = 50000;
@Getter
@Setter
- public int jsonRpcMaxBatchSize = 10;
+ public int jsonRpcMaxBatchSize = 100;
@Getter
@Setter
public int jsonRpcMaxResponseSize = 25 * 1024 * 1024;
diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
index b603d27032b..9e44da25c0d 100644
--- a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
+++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
@@ -103,4 +103,4 @@ public void commitToResponse() throws IOException {
actual.getOutputStream().write(bytes);
actual.getOutputStream().flush();
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java
index efee8d9574e..fcda7d34f86 100644
--- a/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java
+++ b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java
@@ -1,6 +1,10 @@
package org.tron.core.services.filter;
+import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
@@ -47,4 +51,11 @@ public void setReadListener(ReadListener readListener) {
}
};
}
+
+ @Override
+ public BufferedReader getReader() {
+ String encoding = getCharacterEncoding();
+ Charset charset = encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8;
+ return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(body), charset));
+ }
}
diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf
index a804ff877ac..95613b5bf3d 100644
--- a/framework/src/main/resources/config.conf
+++ b/framework/src/main/resources/config.conf
@@ -378,7 +378,7 @@ node {
# Allowed maximum number for blockFilter
maxBlockFilterNum = 50000
# Allowed batch size
- maxBatchSize = 10
+ maxBatchSize = 100
# Allowed max response byte size
maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B
}
From a0f51f8b11227681ab85cc671240b8c5b2e286c2 Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Thu, 23 Apr 2026 17:57:38 +0800
Subject: [PATCH 10/13] add getWriter() for BufferedResponseWrapper
---
.../tron/core/services/filter/BufferedResponseWrapper.java | 6 ++++++
.../java/org/tron/core/services/jsonrpc/JsonRpcServlet.java | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
index 9e44da25c0d..b7a86bafc7b 100644
--- a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
+++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
@@ -2,6 +2,7 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.PrintWriter;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
@@ -82,6 +83,11 @@ public ServletOutputStream getOutputStream() {
return outputStream;
}
+ @Override
+ public PrintWriter getWriter() {
+ return new PrintWriter(outputStream, true);
+ }
+
/**
* Suppress forwarding Content-Length to the real response; caller sets it after size check.
*/
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
index a78eec974f1..c345def2d9d 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
@@ -93,7 +93,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
body = readBody(req.getInputStream());
rootNode = MAPPER.readTree(body);
} catch (IOException e) {
- writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse json error", null);
+ writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null);
return;
}
if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) {
From 38edfda4f1f988647b988dc17e12c163c841df97 Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Mon, 27 Apr 2026 00:07:43 +0800
Subject: [PATCH 11/13] use overflow to replace exception
---
.../JsonRpcResponseTooLargeException.java | 17 --
.../filter/BufferedResponseWrapper.java | 69 +++++---
.../core/services/jsonrpc/JsonRpcServlet.java | 12 +-
.../filter/BufferedResponseWrapperTest.java | 165 ++++++++++++++++++
4 files changed, 216 insertions(+), 47 deletions(-)
delete mode 100644 common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java
create mode 100644 framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java
diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java
deleted file mode 100644
index 65c7bc28cf8..00000000000
--- a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.tron.core.exception.jsonrpc;
-
-public class JsonRpcResponseTooLargeException extends RuntimeException {
-
- public JsonRpcResponseTooLargeException() {
- super();
- }
-
- public JsonRpcResponseTooLargeException(String message) {
- super(message);
- }
-
- public JsonRpcResponseTooLargeException(String message, Throwable cause) {
- super(message, cause);
- }
-
-}
diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
index b7a86bafc7b..01beb05fba2 100644
--- a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
+++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
@@ -7,15 +7,15 @@
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
-import org.tron.core.exception.jsonrpc.JsonRpcResponseTooLargeException;
+import lombok.Getter;
/**
* Buffers the response body without writing to the underlying response,
- * so the caller can inspect the size before committing.
+ * so the caller can replay it after the handler returns.
*
- * If {@code maxBytes > 0}, writes that would push the buffer past {@code maxBytes} throw
- * {@link JsonRpcResponseTooLargeException} immediately, bounding memory usage to at most
- * {@code maxBytes} rather than the full response size.
+ * If {@code maxBytes > 0} and the response would exceed that limit, the
+ * {@link #isOverflow()} flag is set instead of throwing. The caller should check this flag after
+ * the handler returns and write its own error response when true.
*
* Header-mutating methods ({@code setStatus}, {@code setContentType}) are buffered here and
* only forwarded to the real response via {@link #commitToResponse()}, preventing a timed-out
@@ -28,16 +28,31 @@ public class BufferedResponseWrapper extends HttpServletResponseWrapper {
private final int maxBytes;
private int status = HttpServletResponse.SC_OK;
private String contentType;
+ @Getter
+ private boolean overflow = false;
+
private final ServletOutputStream outputStream = new ServletOutputStream() {
@Override
public void write(int b) {
- checkLimit(1);
+ if (overflow) {
+ return;
+ }
+ if (maxBytes > 0 && buffer.size() >= maxBytes) {
+ markOverflow();
+ return;
+ }
buffer.write(b);
}
@Override
public void write(byte[] b, int off, int len) {
- checkLimit(len);
+ if (overflow) {
+ return;
+ }
+ if (maxBytes > 0 && buffer.size() + len > maxBytes) {
+ markOverflow();
+ return;
+ }
buffer.write(b, off, len);
}
@@ -61,10 +76,26 @@ public BufferedResponseWrapper(HttpServletResponse response, int maxBytes) {
this.maxBytes = maxBytes;
}
- private void checkLimit(int incoming) {
- if (maxBytes > 0 && buffer.size() + incoming > maxBytes) {
- throw new JsonRpcResponseTooLargeException(
- "Response byte size exceeds the limit of " + maxBytes);
+ private void markOverflow() {
+ overflow = true;
+ buffer.reset();
+ }
+
+ /**
+ * Early-detection path: if the framework reports the full content length before writing any
+ * bytes, we can flag overflow without buffering anything.
+ */
+ @Override
+ public void setContentLength(int len) {
+ if (maxBytes > 0 && len > maxBytes) {
+ markOverflow();
+ }
+ }
+
+ @Override
+ public void setContentLengthLong(long len) {
+ if (maxBytes > 0 && len > maxBytes) {
+ markOverflow();
}
}
@@ -88,25 +119,13 @@ public PrintWriter getWriter() {
return new PrintWriter(outputStream, true);
}
- /**
- * Suppress forwarding Content-Length to the real response; caller sets it after size check.
- */
- @Override
- public void setContentLength(int len) {
- }
-
- @Override
- public void setContentLengthLong(long len) {
- }
-
public void commitToResponse() throws IOException {
if (contentType != null) {
actual.setContentType(contentType);
}
actual.setStatus(status);
- byte[] bytes = buffer.toByteArray();
- actual.setContentLength(bytes.length);
- actual.getOutputStream().write(bytes);
+ actual.setContentLength(buffer.size());
+ buffer.writeTo(actual.getOutputStream());
actual.getOutputStream().flush();
}
}
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
index c345def2d9d..9bb4901d67c 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
@@ -19,7 +19,6 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.tron.common.parameter.CommonParameter;
-import org.tron.core.exception.jsonrpc.JsonRpcResponseTooLargeException;
import org.tron.core.services.filter.BufferedResponseWrapper;
import org.tron.core.services.filter.CachedBodyRequestWrapper;
import org.tron.core.services.http.RateLimiterServlet;
@@ -109,14 +108,17 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
try {
rpcServer.handle(cachedReq, bufferedResp);
- } catch (JsonRpcResponseTooLargeException e) {
- JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null;
- writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, e.getMessage(), idNode);
- return;
} catch (Exception e) {
throw new IOException("RPC execution failed", e);
}
+ if (bufferedResp.isOverflow()) {
+ JsonNode idNode = !rootNode.isArray() ? rootNode.get("id") : null;
+ writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE,
+ "Response exceeds the limit of " + parameter.getJsonRpcMaxResponseSize() + " bytes",
+ idNode);
+ return;
+ }
bufferedResp.commitToResponse();
}
diff --git a/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java b/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java
new file mode 100644
index 00000000000..cf57866e3ab
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java
@@ -0,0 +1,165 @@
+package org.tron.core.services.filter;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+public class BufferedResponseWrapperTest {
+
+ private MockHttpServletResponse mockResp;
+
+ @Before
+ public void setUp() {
+ mockResp = new MockHttpServletResponse();
+ }
+
+ // --- isOverflow: false cases ---
+
+ @Test
+ public void noLimit_neverOverflows() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.getOutputStream().write(new byte[1024 * 1024]);
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void withinLimit_notOverflow() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 10);
+ w.getOutputStream().write(new byte[10]);
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void exactlyAtLimit_notOverflow() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 5);
+ w.getOutputStream().write(new byte[]{1, 2, 3, 4, 5});
+ assertFalse(w.isOverflow());
+ }
+
+ // --- isOverflow: true via write ---
+
+ @Test
+ public void oneBytePastLimit_overflow() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 5);
+ w.getOutputStream().write(new byte[]{1, 2, 3, 4, 5, 6});
+ assertTrue(w.isOverflow());
+ }
+
+ @Test
+ public void singleByteWrite_triggerOverflow() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 3);
+ w.getOutputStream().write(1);
+ w.getOutputStream().write(2);
+ w.getOutputStream().write(3);
+ assertFalse(w.isOverflow());
+ w.getOutputStream().write(4);
+ assertTrue(w.isOverflow());
+ }
+
+ @Test
+ public void overflow_bufferIsReleasedOnOverflow() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 4);
+ w.getOutputStream().write(new byte[]{1, 2, 3, 4, 5});
+ assertTrue(w.isOverflow());
+ // After overflow, further writes are silently discarded — no exception
+ w.getOutputStream().write(new byte[100]);
+ assertTrue(w.isOverflow());
+ }
+
+ // --- isOverflow: true via setContentLength ---
+
+ @Test
+ public void setContentLength_exceedsLimit_overflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setContentLength(101);
+ assertTrue(w.isOverflow());
+ }
+
+ @Test
+ public void setContentLength_exactlyAtLimit_notOverflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setContentLength(100);
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void setContentLengthLong_exceedsLimit_overflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setContentLengthLong(101L);
+ assertTrue(w.isOverflow());
+ }
+
+ @Test
+ public void setContentLength_noLimit_neverOverflows() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.setContentLength(Integer.MAX_VALUE);
+ assertFalse(w.isOverflow());
+ }
+
+ // --- setContentLength early detection: writes after early overflow are discarded ---
+
+ @Test
+ public void earlyOverflow_subsequentWritesDiscarded() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 10);
+ w.setContentLength(20);
+ assertTrue(w.isOverflow());
+ w.getOutputStream().write(new byte[5]);
+ // Nothing committed to actual response
+ assertFalse(mockResp.isCommitted());
+ }
+
+ // --- commitToResponse ---
+
+ @Test
+ public void commitToResponse_writesBodyAndHeaders() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ byte[] data = "hello".getBytes(StandardCharsets.UTF_8);
+ w.setStatus(200);
+ w.setContentType("application/json");
+ w.getOutputStream().write(data);
+ w.commitToResponse();
+
+ assertEquals(200, mockResp.getStatus());
+ assertEquals("application/json", mockResp.getContentType());
+ assertArrayEquals(data, mockResp.getContentAsByteArray());
+ }
+
+ @Test
+ public void commitToResponse_setsCorrectContentLength() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ byte[] data = new byte[]{10, 20, 30};
+ w.getOutputStream().write(data);
+ w.commitToResponse();
+
+ assertEquals(3, mockResp.getContentLength());
+ }
+
+ @Test
+ public void commitToResponse_emptyBuffer_writesZeroBytes() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setStatus(200);
+ w.commitToResponse();
+
+ assertEquals(0, mockResp.getContentLength());
+ assertEquals(0, mockResp.getContentAsByteArray().length);
+ }
+
+ // --- header buffering: nothing reaches actual response until commit ---
+
+ @Test
+ public void statusNotForwardedBeforeCommit() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.setStatus(201);
+ // MockHttpServletResponse defaults to 200
+ assertEquals(200, mockResp.getStatus());
+ w.commitToResponse();
+ assertEquals(201, mockResp.getStatus());
+ }
+}
From e70ea6b8363c885a69b3a13abb1368fc7d79f1c7 Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Mon, 27 Apr 2026 00:21:06 +0800
Subject: [PATCH 12/13] reuse the PrintWriter
---
.../tron/core/services/filter/BufferedResponseWrapper.java | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
index 01beb05fba2..46872b15e21 100644
--- a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
+++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
@@ -66,6 +66,8 @@ public void setWriteListener(WriteListener writeListener) {
}
};
+ private final PrintWriter writer = new PrintWriter(outputStream, true);
+
/**
* @param response the wrapped response
* @param maxBytes max allowed response bytes; {@code 0} means no limit
@@ -116,7 +118,7 @@ public ServletOutputStream getOutputStream() {
@Override
public PrintWriter getWriter() {
- return new PrintWriter(outputStream, true);
+ return writer;
}
public void commitToResponse() throws IOException {
From bf9a5a13b8e27d9cac5e84317ce415f9ca5696b4 Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Tue, 28 Apr 2026 23:33:44 +0800
Subject: [PATCH 13/13] fix default maxResponseSize
---
common/src/main/resources/reference.conf | 4 ++--
.../core/services/jsonrpc/JsonRpcServlet.java | 6 +++---
framework/src/main/resources/config.conf | 15 ++++++---------
3 files changed, 11 insertions(+), 14 deletions(-)
diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf
index b33e872b68e..dc1d4ee91ac 100644
--- a/common/src/main/resources/reference.conf
+++ b/common/src/main/resources/reference.conf
@@ -404,10 +404,10 @@ node {
# Maximum number of requests in a JSON-RPC batch, >0 otherwise no limit
maxBatchSize = 100
- # Maximum response body size in bytes for JSON-RPC (default 25MB)
+ # Maximum response body size in bytes for JSON-RPC (default 25MB), >0 otherwise no limit
maxResponseSize = 26214400
- # Maximum number of addresses in a single JSON-RPC request
+ # Maximum number of addresses in a single JSON-RPC request, >0 otherwise no limit
maxAddressSize = 1000
}
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
index 9bb4901d67c..941961cd32b 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
@@ -95,10 +95,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null);
return;
}
- if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) {
+ int batchSize = parameter.getJsonRpcMaxBatchSize();
+ if (rootNode.isArray() && batchSize > 0 && rootNode.size() > batchSize) {
writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT,
- "Batch size " + rootNode.size() + " exceeds the limit of "
- + parameter.getJsonRpcMaxBatchSize(), null);
+ "Batch size " + rootNode.size() + " exceeds the limit of " + batchSize, null);
return;
}
diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf
index 95613b5bf3d..1ae2d30c2e5 100644
--- a/framework/src/main/resources/config.conf
+++ b/framework/src/main/resources/config.conf
@@ -366,20 +366,17 @@ node {
# httpPBFTEnable = false
# httpPBFTPort = 8565
- # The maximum blocks range to retrieve logs for eth_getLogs, default value is 5000,
- # should be > 0, otherwise means no limit.
+ # The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, >0 otherwise no limit
maxBlockRange = 5000
- # Allowed max address count in filter request, default value is 1000,
- # should be > 0, otherwise means no limit.
+ # Allowed max address count in filter request, default: 1000, >0 otherwise no limit
maxAddressSize = 1000
- # The maximum number of allowed topics within a topic criteria, default value is 1000,
- # should be > 0, otherwise means no limit.
+ # The maximum number of allowed topics within a topic criteria, default: 1000, >0 otherwise no limit
maxSubTopics = 1000
- # Allowed maximum number for blockFilter
+ # Allowed maximum number for blockFilter, default: 50000, >0 otherwise no limit
maxBlockFilterNum = 50000
- # Allowed batch size
+ # Allowed batch size, default: 100, default: 100, >0 otherwise no limit
maxBatchSize = 100
- # Allowed max response byte size
+ # Allowed max response byte size, default: 26214400, >0 otherwise no limit
maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B
}