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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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
}
From 9eb44ceabb5ec8a99e37aa8d1c7c077c420fd3ae Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Wed, 6 May 2026 23:52:03 +0800
Subject: [PATCH 14/22] optimize JsonRpcServlet
---
.../filter/BufferedResponseWrapper.java | 10 +++-
.../core/services/jsonrpc/JsonRpcServlet.java | 57 ++++++++++++-------
framework/src/main/resources/config.conf | 6 +-
3 files changed, 48 insertions(+), 25 deletions(-)
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 46872b15e21..2f2522b8820 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
@@ -18,8 +18,7 @@
* 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
- * handler thread from racing with the timeout error writer.
+ * only forwarded to the real response via {@link #commitToResponse()}.
*/
public class BufferedResponseWrapper extends HttpServletResponseWrapper {
@@ -28,8 +27,9 @@ public class BufferedResponseWrapper extends HttpServletResponseWrapper {
private final int maxBytes;
private int status = HttpServletResponse.SC_OK;
private String contentType;
+ private boolean committed = false;
@Getter
- private boolean overflow = false;
+ private volatile boolean overflow = false;
private final ServletOutputStream outputStream = new ServletOutputStream() {
@Override
@@ -122,6 +122,10 @@ public PrintWriter getWriter() {
}
public void commitToResponse() throws IOException {
+ if (committed) {
+ throw new IllegalStateException("commitToResponse() already called");
+ }
+ committed = true;
if (contentType != null) {
actual.setContentType(contentType);
}
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 941961cd32b..fdbb9685f57 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,7 +1,9 @@
package org.tron.core.services.jsonrpc;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.googlecode.jsonrpc4j.HttpStatusCodeProvider;
import com.googlecode.jsonrpc4j.JsonRpcInterceptor;
@@ -29,12 +31,13 @@ public class JsonRpcServlet extends RateLimiterServlet {
private static final ObjectMapper MAPPER = new ObjectMapper();
- enum JsonRpcError {
+ private enum JsonRpcError {
PARSE_ERROR(-32700),
+ INTERNAL_ERROR(-32603),
EXCEED_LIMIT(-32005),
RESPONSE_TOO_LARGE(-32003);
- final int code;
+ private final int code;
JsonRpcError(int code) {
this.code = code;
@@ -86,19 +89,25 @@ public Integer getJsonRpcCode(int httpStatusCode) {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
CommonParameter parameter = CommonParameter.getInstance();
- byte[] body;
+ // Transport IOException from readBody propagates as HTTP 500 (genuine IO failure).
+ byte[] body = readBody(req.getInputStream());
JsonNode rootNode;
try {
- body = readBody(req.getInputStream());
rootNode = MAPPER.readTree(body);
- } catch (IOException e) {
- writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null);
+ if (rootNode == null || rootNode.isMissingNode()) {
+ writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false);
+ return;
+ }
+ } catch (JsonProcessingException e) {
+ writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false);
return;
}
+
+ boolean isBatch = rootNode.isArray();
int batchSize = parameter.getJsonRpcMaxBatchSize();
- if (rootNode.isArray() && batchSize > 0 && rootNode.size() > batchSize) {
+ if (isBatch && batchSize > 0 && rootNode.size() > batchSize) {
writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT,
- "Batch size " + rootNode.size() + " exceeds the limit of " + batchSize, null);
+ "Batch size " + rootNode.size() + " exceeds the limit of " + batchSize, null, true);
return;
}
@@ -108,15 +117,18 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
try {
rpcServer.handle(cachedReq, bufferedResp);
- } catch (Exception e) {
- throw new IOException("RPC execution failed", e);
+ } catch (RuntimeException e) {
+ logger.error("RPC execution failed", e);
+ JsonNode idNode = isBatch ? null : rootNode.get("id");
+ writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", idNode, isBatch);
+ return;
}
if (bufferedResp.isOverflow()) {
- JsonNode idNode = !rootNode.isArray() ? rootNode.get("id") : null;
+ JsonNode idNode = isBatch ? null : rootNode.get("id");
writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE,
"Response exceeds the limit of " + parameter.getJsonRpcMaxResponseSize() + " bytes",
- idNode);
+ idNode, isBatch);
return;
}
bufferedResp.commitToResponse();
@@ -133,18 +145,25 @@ private byte[] readBody(InputStream in) throws IOException {
}
private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, String message,
- JsonNode id) throws IOException {
- ObjectNode root = MAPPER.createObjectNode();
- root.put("jsonrpc", "2.0");
- ObjectNode errNode = root.putObject("error");
+ JsonNode id, boolean isBatch) throws IOException {
+ ObjectNode errorObj = MAPPER.createObjectNode();
+ errorObj.put("jsonrpc", "2.0");
+ ObjectNode errNode = errorObj.putObject("error");
errNode.put("code", error.code);
errNode.put("message", message);
if (id != null && !id.isNull() && !id.isMissingNode()) {
- root.set("id", id);
+ errorObj.set("id", id);
+ } else {
+ errorObj.putNull("id");
+ }
+ byte[] bytes;
+ if (isBatch) {
+ ArrayNode arr = MAPPER.createArrayNode();
+ arr.add(errorObj);
+ bytes = MAPPER.writeValueAsBytes(arr);
} else {
- root.putNull("id");
+ bytes = MAPPER.writeValueAsBytes(errorObj);
}
- byte[] bytes = MAPPER.writeValueAsBytes(root);
resp.setContentType("application/json; charset=utf-8");
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 1ae2d30c2e5..2b27c8069e2 100644
--- a/framework/src/main/resources/config.conf
+++ b/framework/src/main/resources/config.conf
@@ -374,10 +374,10 @@ node {
maxSubTopics = 1000
# Allowed maximum number for blockFilter, default: 50000, >0 otherwise no limit
maxBlockFilterNum = 50000
- # Allowed batch size, default: 100, default: 100, >0 otherwise no limit
+ # Allowed batch size, default: 100, >0 otherwise no limit
maxBatchSize = 100
- # Allowed max response byte size, default: 26214400, >0 otherwise no limit
- maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B
+ # Allowed max response byte size, default: 26214400 (25 MB), >0 otherwise no limit
+ maxResponseSize = 26214400
}
# Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode,
From d65f06416cca7a665972454d0f9c3fc2a351ad9b Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Thu, 7 May 2026 00:23:48 +0800
Subject: [PATCH 15/22] don't invoke getInputStream and getReader in
HttpServletRequestWrapper twice; add several methods of
HttpServletRequestWrapper
---
.../filter/BufferedResponseWrapper.java | 31 +++++++++++++++++++
.../filter/CachedBodyRequestWrapper.java | 11 +++++++
framework/src/main/resources/config.conf | 12 +++----
3 files changed, 48 insertions(+), 6 deletions(-)
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 2f2522b8820..a4cf777d85a 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
@@ -101,11 +101,42 @@ public void setContentLengthLong(long len) {
}
}
+ @Override
+ public int getStatus() {
+ return this.status;
+ }
+
@Override
public void setStatus(int sc) {
this.status = sc;
}
+ @Override
+ public void setHeader(String name, String value) {
+ if ("content-length".equalsIgnoreCase(name)) {
+ try {
+ setContentLengthLong(Long.parseLong(value));
+ } catch (NumberFormatException ignored) {
+ // malformed value, skip overflow check
+ }
+ } else {
+ super.setHeader(name, value);
+ }
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ if ("content-length".equalsIgnoreCase(name)) {
+ try {
+ setContentLengthLong(Long.parseLong(value));
+ } catch (NumberFormatException ignored) {
+ // malformed value, skip overflow check
+ }
+ } else {
+ super.addHeader(name, value);
+ }
+ }
+
@Override
public void setContentType(String type) {
this.contentType = type;
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 fcda7d34f86..1b0141b2e5b 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
@@ -15,7 +15,10 @@
*/
public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
+ private enum BodyAccessor { NONE, STREAM, READER }
+
private final byte[] body;
+ private BodyAccessor accessor = BodyAccessor.NONE;
public CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) {
super(request);
@@ -24,6 +27,10 @@ public CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) {
@Override
public ServletInputStream getInputStream() {
+ if (accessor == BodyAccessor.READER) {
+ throw new IllegalStateException("getReader() has already been called on this request");
+ }
+ accessor = BodyAccessor.STREAM;
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
@@ -54,6 +61,10 @@ public void setReadListener(ReadListener readListener) {
@Override
public BufferedReader getReader() {
+ if (accessor == BodyAccessor.STREAM) {
+ throw new IllegalStateException("getInputStream() has already been called on this request");
+ }
+ accessor = BodyAccessor.READER;
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 2b27c8069e2..15149dcb6d8 100644
--- a/framework/src/main/resources/config.conf
+++ b/framework/src/main/resources/config.conf
@@ -366,17 +366,17 @@ node {
# httpPBFTEnable = false
# httpPBFTPort = 8565
- # The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, >0 otherwise no limit
+ # The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, <=0 means no limit
maxBlockRange = 5000
- # Allowed max address count in filter request, default: 1000, >0 otherwise no limit
+ # Allowed max address count in filter request, default: 1000, <=0 means no limit
maxAddressSize = 1000
- # The maximum number of allowed topics within a topic criteria, default: 1000, >0 otherwise no limit
+ # The maximum number of allowed topics within a topic criteria, default: 1000, <=0 means no limit
maxSubTopics = 1000
- # Allowed maximum number for blockFilter, default: 50000, >0 otherwise no limit
+ # Allowed maximum number for blockFilter, default: 50000, <=0 means no limit
maxBlockFilterNum = 50000
- # Allowed batch size, default: 100, >0 otherwise no limit
+ # Allowed batch size, default: 100, <=0 means no limit
maxBatchSize = 100
- # Allowed max response byte size, default: 26214400 (25 MB), >0 otherwise no limit
+ # Allowed max response byte size, default: 26214400 (25 MB), <=0 means no limit
maxResponseSize = 26214400
}
From 801e7ae6f0feba924d3054a415cc7762db7282a9 Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Thu, 7 May 2026 10:53:07 +0800
Subject: [PATCH 16/22] add testcase of JsonRpcServlet and
CachedBodyRequestWrapper
---
framework/build.gradle | 1 +
.../filter/BufferedResponseWrapperTest.java | 99 ++++++++
.../filter/CachedBodyRequestWrapperTest.java | 108 +++++++++
.../services/jsonrpc/JsonRpcServletTest.java | 215 ++++++++++++++++++
4 files changed, 423 insertions(+)
create mode 100644 framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java
create mode 100644 framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
diff --git a/framework/build.gradle b/framework/build.gradle
index d884b6a7c49..49fe02e2821 100644
--- a/framework/build.gradle
+++ b/framework/build.gradle
@@ -58,6 +58,7 @@ dependencies {
}
testImplementation group: 'org.springframework', name: 'spring-test', version: "${springVersion}"
+ testImplementation group: 'javax.portlet', name: 'portlet-api', version: '3.0.1'
implementation group: 'org.zeromq', name: 'jeromq', version: '0.5.3'
api project(":chainbase")
api project(":protocol")
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
index cf57866e3ab..76de2d2db2e 100644
--- a/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java
+++ b/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java
@@ -3,6 +3,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
@@ -162,4 +163,102 @@ public void statusNotForwardedBeforeCommit() throws IOException {
w.commitToResponse();
assertEquals(201, mockResp.getStatus());
}
+
+ // --- getStatus() ---
+
+ @Test
+ public void getStatus_returnsBufferedValue() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.setStatus(404);
+ assertEquals(404, w.getStatus());
+ // actual response must still be untouched
+ assertEquals(200, mockResp.getStatus());
+ }
+
+ @Test
+ public void getStatus_defaultIs200() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ assertEquals(200, w.getStatus());
+ }
+
+ // --- setHeader / addHeader for Content-Length ---
+
+ @Test
+ public void setHeader_contentLength_exceedsLimit_overflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setHeader("Content-Length", "101");
+ assertTrue(w.isOverflow());
+ // Content-Length must NOT have been forwarded to the actual response
+ assertNull(mockResp.getHeader("Content-Length"));
+ }
+
+ @Test
+ public void setHeader_contentLength_withinLimit_noOverflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setHeader("Content-Length", "100");
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void setHeader_contentLength_caseInsensitive_overflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 50);
+ w.setHeader("content-length", "51");
+ assertTrue(w.isOverflow());
+ }
+
+ @Test
+ public void setHeader_contentLength_malformed_ignored() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setHeader("Content-Length", "not-a-number");
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void setHeader_nonContentLength_passesThroughToActual() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.setHeader("X-Custom-Header", "hello");
+ assertEquals("hello", mockResp.getHeader("X-Custom-Header"));
+ }
+
+ @Test
+ public void addHeader_contentLength_exceedsLimit_overflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.addHeader("Content-Length", "200");
+ assertTrue(w.isOverflow());
+ assertNull(mockResp.getHeader("Content-Length"));
+ }
+
+ @Test
+ public void addHeader_contentLength_malformed_ignored() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.addHeader("Content-Length", "bad");
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void addHeader_nonContentLength_passesThroughToActual() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.addHeader("X-Trace-Id", "abc123");
+ assertEquals("abc123", mockResp.getHeader("X-Trace-Id"));
+ }
+
+ // --- commitToResponse idempotency ---
+
+ @Test(expected = IllegalStateException.class)
+ public void commitToResponse_secondCall_throwsIllegalState() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.commitToResponse();
+ w.commitToResponse();
+ }
+
+ // --- getWriter path ---
+
+ @Test
+ public void writeViaWriter_commitToResponse_flushesBody() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.getWriter().print("hello");
+ w.getWriter().flush();
+ w.commitToResponse();
+ assertEquals("hello", mockResp.getContentAsString());
+ }
}
diff --git a/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java b/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java
new file mode 100644
index 00000000000..85915895e8d
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java
@@ -0,0 +1,108 @@
+package org.tron.core.services.filter;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+public class CachedBodyRequestWrapperTest {
+
+ private static final byte[] BODY = "hello world".getBytes(StandardCharsets.UTF_8);
+
+ private static byte[] readFully(javax.servlet.ServletInputStream in) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buf = new byte[128];
+ int n;
+ while ((n = in.read(buf)) != -1) {
+ out.write(buf, 0, n);
+ }
+ return out.toByteArray();
+ }
+
+ // --- getInputStream ---
+
+ @Test
+ public void getInputStream_returnsBodyContent() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ byte[] read = readFully(w.getInputStream());
+ assertEquals(new String(BODY, StandardCharsets.UTF_8), new String(read, StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void getInputStream_calledTwice_bothSucceed() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ w.getInputStream();
+ // second call of the same accessor is allowed by the servlet spec
+ w.getInputStream();
+ }
+
+ // --- getReader ---
+
+ @Test
+ public void getReader_returnsBodyContent() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ String line = w.getReader().readLine();
+ assertEquals("hello world", line);
+ }
+
+ @Test
+ public void getReader_calledTwice_bothSucceed() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ w.getReader();
+ w.getReader();
+ }
+
+ // --- mutual exclusion ---
+
+ @Test(expected = IllegalStateException.class)
+ public void getReader_afterGetInputStream_throws() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ w.getInputStream();
+ w.getReader();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void getInputStream_afterGetReader_throws() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ w.getReader();
+ w.getInputStream();
+ }
+
+ // --- stream contract ---
+
+ @Test
+ public void getInputStream_isFinished_afterFullRead() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ javax.servlet.ServletInputStream in = w.getInputStream();
+ while (in.read() != -1) {
+ // drain
+ }
+ assertTrue(in.isFinished());
+ }
+
+ @Test
+ public void getInputStream_isReady_returnsTrue() {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ assertTrue(w.getInputStream().isReady());
+ }
+
+ @Test
+ public void getInputStream_emptyBody_isFinishedImmediately() {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(),
+ new byte[0]);
+ assertTrue(w.getInputStream().isFinished());
+ }
+
+ @Test
+ public void getReader_usesRequestCharacterEncoding() throws IOException {
+ MockHttpServletRequest req = new MockHttpServletRequest();
+ req.setCharacterEncoding("UTF-8");
+ byte[] utf8Body = "tron".getBytes(StandardCharsets.UTF_8);
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(req, utf8Body);
+ assertEquals("tron", w.getReader().readLine());
+ }
+}
diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
new file mode 100644
index 00000000000..5d10e16d76b
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
@@ -0,0 +1,215 @@
+package org.tron.core.services.jsonrpc;
+
+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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.googlecode.jsonrpc4j.JsonRpcServer;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.tron.common.parameter.CommonParameter;
+
+public class JsonRpcServletTest {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private TestableServlet servlet;
+ private JsonRpcServer mockRpcServer;
+ private int savedMaxBatchSize;
+ private int savedMaxResponseSize;
+
+ @Before
+ public void setUp() throws Exception {
+ servlet = new TestableServlet();
+ mockRpcServer = mock(JsonRpcServer.class);
+ Field f = JsonRpcServlet.class.getDeclaredField("rpcServer");
+ f.setAccessible(true);
+ f.set(servlet, mockRpcServer);
+ savedMaxBatchSize = CommonParameter.getInstance().jsonRpcMaxBatchSize;
+ savedMaxResponseSize = CommonParameter.getInstance().jsonRpcMaxResponseSize;
+ }
+
+ @After
+ public void tearDown() {
+ CommonParameter.getInstance().jsonRpcMaxBatchSize = savedMaxBatchSize;
+ CommonParameter.getInstance().jsonRpcMaxResponseSize = savedMaxResponseSize;
+ }
+
+ // --- parse error paths ---
+
+ @Test
+ public void invalidJson_returnsParseError() throws Exception {
+ MockHttpServletResponse resp = doPost("not {{ valid json");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertFalse(body.isArray());
+ assertEquals(-32700, body.get("error").get("code").asInt());
+ assertEquals("2.0", body.get("jsonrpc").asText());
+ assertTrue(body.get("id").isNull());
+ }
+
+ @Test
+ public void emptyBody_returnsParseError() throws Exception {
+ MockHttpServletResponse resp = doPost("");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals(-32700, body.get("error").get("code").asInt());
+ }
+
+ // --- batch size limit ---
+
+ @Test
+ public void batchExceedsLimit_returnsExceedLimitAsArray() throws Exception {
+ CommonParameter.getInstance().jsonRpcMaxBatchSize = 2;
+ MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2},{\"id\":3}]");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertTrue("batch error response must be a JSON array", body.isArray());
+ assertEquals(1, body.size());
+ assertEquals(-32005, body.get(0).get("error").get("code").asInt());
+ }
+
+ @Test
+ public void batchWithinLimit_proceedsToRpcServer() throws Exception {
+ CommonParameter.getInstance().jsonRpcMaxBatchSize = 5;
+ byte[] rpcResp = "[{\"result\":\"ok\"}]".getBytes(StandardCharsets.UTF_8);
+ doAnswer(inv -> {
+ HttpServletResponse r = inv.getArgument(1);
+ r.getOutputStream().write(rpcResp);
+ return null;
+ }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+
+ MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2}]");
+ assertEquals(200, resp.getStatus());
+ assertArrayEquals(rpcResp, resp.getContentAsByteArray());
+ }
+
+ @Test
+ public void batchLimitDisabled_largeBatchAllowed() throws Exception {
+ CommonParameter.getInstance().jsonRpcMaxBatchSize = 0;
+ byte[] rpcResp = "[]".getBytes(StandardCharsets.UTF_8);
+ doAnswer(inv -> {
+ HttpServletResponse r = inv.getArgument(1);
+ r.getOutputStream().write(rpcResp);
+ return null;
+ }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+
+ StringBuilder sb = new StringBuilder("[");
+ for (int i = 0; i < 500; i++) {
+ if (i > 0) {
+ sb.append(',');
+ }
+ sb.append("{}");
+ }
+ sb.append("]");
+ MockHttpServletResponse resp = doPost(sb.toString());
+ assertEquals(200, resp.getStatus());
+ assertArrayEquals(rpcResp, resp.getContentAsByteArray());
+ }
+
+ // --- rpcServer.handle exceptions ---
+
+ @Test
+ public void rpcServerThrowsRuntimeException_returnsInternalError() throws Exception {
+ doThrow(new RuntimeException("server exploded")).when(mockRpcServer)
+ .handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+ MockHttpServletResponse resp = doPost("{\"method\":\"eth_blockNumber\",\"id\":42}");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertFalse(body.isArray());
+ assertEquals(-32603, body.get("error").get("code").asInt());
+ }
+
+ @Test
+ public void batchRpcServerThrows_internalErrorIsArray() throws Exception {
+ doThrow(new RuntimeException("boom")).when(mockRpcServer).handle((HttpServletRequest) any(), any());
+ MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertTrue("batch internal error must be an array", body.isArray());
+ assertEquals(-32603, body.get(0).get("error").get("code").asInt());
+ }
+
+ // --- response size limit ---
+
+ @Test
+ public void responseTooLarge_returnsSingleErrorObject() throws Exception {
+ int limit = 50;
+ CommonParameter.getInstance().jsonRpcMaxResponseSize = limit;
+ doAnswer(inv -> {
+ HttpServletResponse r = inv.getArgument(1);
+ r.getOutputStream().write(new byte[limit + 1]);
+ return null;
+ }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+
+ MockHttpServletResponse resp = doPost("{\"method\":\"eth_getLogs\",\"id\":1}");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertFalse(body.isArray());
+ assertEquals(-32003, body.get("error").get("code").asInt());
+ }
+
+ @Test
+ public void batchResponseTooLarge_returnsErrorArray() throws Exception {
+ int limit = 50;
+ CommonParameter.getInstance().jsonRpcMaxResponseSize = limit;
+ doAnswer(inv -> {
+ HttpServletResponse r = inv.getArgument(1);
+ r.getOutputStream().write(new byte[limit + 1]);
+ return null;
+ }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+
+ MockHttpServletResponse resp = doPost("[{\"method\":\"eth_getLogs\"}]");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertTrue("batch response-too-large must be an array", body.isArray());
+ assertEquals(-32003, body.get(0).get("error").get("code").asInt());
+ }
+
+ // --- normal path ---
+
+ @Test
+ public void normalRequest_commitsRpcServerResponse() throws Exception {
+ byte[] rpcResp = "{\"result\":\"0x1\"}".getBytes(StandardCharsets.UTF_8);
+ doAnswer(inv -> {
+ HttpServletResponse r = inv.getArgument(1);
+ r.getOutputStream().write(rpcResp);
+ return null;
+ }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+
+ MockHttpServletResponse resp = doPost("{\"method\":\"eth_blockNumber\",\"id\":1}");
+ assertEquals(200, resp.getStatus());
+ assertArrayEquals(rpcResp, resp.getContentAsByteArray());
+ }
+
+ // --- helpers ---
+
+ private MockHttpServletResponse doPost(String body) throws Exception {
+ MockHttpServletRequest req = new MockHttpServletRequest("POST", "/jsonrpc");
+ req.setContent(body.getBytes(StandardCharsets.UTF_8));
+ MockHttpServletResponse resp = new MockHttpServletResponse();
+ servlet.callDoPost(req, resp);
+ return resp;
+ }
+
+ private static class TestableServlet extends JsonRpcServlet {
+ void callDoPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ doPost(req, resp);
+ }
+ }
+}
From 2bb35a860344ad0aae06b624b031ff3da10063f9 Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Thu, 7 May 2026 10:56:06 +0800
Subject: [PATCH 17/22] format code of testcase
---
.../core/services/filter/CachedBodyRequestWrapperTest.java | 3 ++-
.../org/tron/core/services/jsonrpc/JsonRpcServletTest.java | 4 +++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java b/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java
index 85915895e8d..813b1a61bea 100644
--- a/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java
+++ b/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java
@@ -29,7 +29,8 @@ private static byte[] readFully(javax.servlet.ServletInputStream in) throws IOEx
public void getInputStream_returnsBodyContent() throws IOException {
CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
byte[] read = readFully(w.getInputStream());
- assertEquals(new String(BODY, StandardCharsets.UTF_8), new String(read, StandardCharsets.UTF_8));
+ assertEquals(new String(BODY, StandardCharsets.UTF_8),
+ new String(read, StandardCharsets.UTF_8));
}
@Test
diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
index 5d10e16d76b..6a6c679070f 100644
--- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
+++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
@@ -137,7 +137,8 @@ public void rpcServerThrowsRuntimeException_returnsInternalError() throws Except
@Test
public void batchRpcServerThrows_internalErrorIsArray() throws Exception {
- doThrow(new RuntimeException("boom")).when(mockRpcServer).handle((HttpServletRequest) any(), any());
+ doThrow(new RuntimeException("boom")).when(mockRpcServer)
+ .handle((HttpServletRequest) any(), any());
MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]");
assertEquals(200, resp.getStatus());
JsonNode body = MAPPER.readTree(resp.getContentAsString());
@@ -208,6 +209,7 @@ private MockHttpServletResponse doPost(String body) throws Exception {
}
private static class TestableServlet extends JsonRpcServlet {
+
void callDoPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
doPost(req, resp);
}
From 4bdc025ac70cd231b91cb27362c22d3ad3fdf2e0 Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Fri, 8 May 2026 15:41:57 +0800
Subject: [PATCH 18/22] reject empty batch; use StandardCharsets.UTF_8
---
.../filter/BufferedResponseWrapper.java | 5 +-
.../filter/CachedBodyRequestWrapper.java | 9 +++-
.../core/services/jsonrpc/JsonRpcServlet.java | 5 ++
.../org/tron/core/jsonrpc/JsonRpcTest.java | 54 +++++++++++++++++++
.../services/jsonrpc/JsonRpcServletTest.java | 11 ++++
5 files changed, 82 insertions(+), 2 deletions(-)
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 a4cf777d85a..e99dcffde67 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,7 +2,9 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.OutputStreamWriter;
import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
@@ -66,7 +68,8 @@ public void setWriteListener(WriteListener writeListener) {
}
};
- private final PrintWriter writer = new PrintWriter(outputStream, true);
+ private final PrintWriter writer =
+ new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8), true);
/**
* @param response the wrapped response
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 1b0141b2e5b..ef33f92d25a 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
@@ -4,7 +4,9 @@
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
@@ -66,7 +68,12 @@ public BufferedReader getReader() {
}
accessor = BodyAccessor.READER;
String encoding = getCharacterEncoding();
- Charset charset = encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8;
+ Charset charset;
+ try {
+ charset = encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8;
+ } catch (IllegalCharsetNameException | UnsupportedCharsetException ex) {
+ charset = StandardCharsets.UTF_8;
+ }
return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(body), charset));
}
}
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 fdbb9685f57..2d2c6f2fed1 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
@@ -33,6 +33,7 @@ public class JsonRpcServlet extends RateLimiterServlet {
private enum JsonRpcError {
PARSE_ERROR(-32700),
+ INVALID_REQUEST(-32600),
INTERNAL_ERROR(-32603),
EXCEED_LIMIT(-32005),
RESPONSE_TOO_LARGE(-32003);
@@ -104,6 +105,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
}
boolean isBatch = rootNode.isArray();
+ if (isBatch && rootNode.isEmpty()) {
+ writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", null, false);
+ return;
+ }
int batchSize = parameter.getJsonRpcMaxBatchSize();
if (isBatch && batchSize > 0 && rootNode.size() > batchSize) {
writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT,
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java
index bd357101da3..598af6f8d3a 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java
@@ -8,8 +8,10 @@
import java.util.ArrayList;
import java.util.BitSet;
+import java.util.Collections;
import java.util.List;
import org.bouncycastle.util.encoders.Hex;
+import org.tron.common.parameter.CommonParameter;
import org.junit.Assert;
import org.junit.Test;
import org.tron.common.bloom.Bloom;
@@ -242,6 +244,58 @@ public void testLogFilter() {
}
}
+ @Test
+ public void testLogFilterAddressSizeLimit() {
+ // Two valid 20-byte addresses (40 hex chars with 0x prefix)
+ String addr1 = "0xaa6612f03443517ced2bdcf27958c22353ceeab9";
+ String addr2 = "0xbb7723a04554628ced3cdf38069b433464ffbc0a";
+ String addr3 = "0xcc8834b15665739def4de049f17a544575aabcd1";
+
+ int savedLimit = CommonParameter.getInstance().jsonRpcMaxAddressSize;
+ try {
+ CommonParameter.getInstance().jsonRpcMaxAddressSize = 2;
+
+ // Exactly at limit — must not throw
+ ArrayList Scope: designed for synchronous, raw-body POST endpoints
+ * (e.g. JSON-RPC). It is NOT compatible with:
+ * Multiple calls to {@code getInputStream()} (or {@code getReader()})
+ * are allowed and each returns a fresh stream over the same cached body —
+ * a deliberate extension of the standard servlet contract.
*/
public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
- private enum BodyAccessor { NONE, STREAM, READER }
+ private enum BodyAccessor {NONE, STREAM, READER}
private final byte[] body;
private BodyAccessor accessor = BodyAccessor.NONE;
@@ -57,6 +73,8 @@ public boolean isReady() {
@Override
public void setReadListener(ReadListener readListener) {
+ throw new UnsupportedOperationException(
+ "async I/O is not supported on cached body");
}
};
}
From c329b470d6ef890f90f5233c93e12a6fbaea7c2b Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Fri, 8 May 2026 22:26:11 +0800
Subject: [PATCH 21/22] format the code
---
.../org/tron/core/services/filter/CachedBodyRequestWrapper.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 ce4278ddecb..683fe849f71 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
@@ -33,7 +33,7 @@
*/
public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
- private enum BodyAccessor {NONE, STREAM, READER}
+ private enum BodyAccessor { NONE, STREAM, READER }
private final byte[] body;
private BodyAccessor accessor = BodyAccessor.NONE;
From 998f52f1d906e1baf5e9eecce23d0a27852d902c Mon Sep 17 00:00:00 2001
From: jiangyuanshu <317787106@qq.com>
Date: Fri, 8 May 2026 23:08:49 +0800
Subject: [PATCH 22/22] don't continue rest jsonrpc request of batch if
overflow
---
.../core/services/jsonrpc/JsonRpcServlet.java | 78 +++++++++++++++++--
.../services/jsonrpc/JsonRpcServletTest.java | 65 +++++++++++-----
2 files changed, 119 insertions(+), 24 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 2d2c6f2fed1..c2f202f1fe5 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
@@ -9,6 +9,7 @@
import com.googlecode.jsonrpc4j.JsonRpcInterceptor;
import com.googlecode.jsonrpc4j.JsonRpcServer;
import com.googlecode.jsonrpc4j.ProxyUtil;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -116,29 +117,94 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
return;
}
+ int maxResponseSize = parameter.getJsonRpcMaxResponseSize();
+ if (isBatch) {
+ handleBatch(resp, rootNode, maxResponseSize);
+ } else {
+ handleSingle(req, resp, rootNode, body, maxResponseSize);
+ }
+ }
+
+ private void handleSingle(HttpServletRequest req, HttpServletResponse resp,
+ JsonNode rootNode, byte[] body, int maxResponseSize) throws IOException {
CachedBodyRequestWrapper cachedReq = new CachedBodyRequestWrapper(req, body);
BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(
- resp, parameter.getJsonRpcMaxResponseSize());
+ resp, maxResponseSize);
try {
rpcServer.handle(cachedReq, bufferedResp);
} catch (RuntimeException e) {
logger.error("RPC execution failed", e);
- JsonNode idNode = isBatch ? null : rootNode.get("id");
- writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", idNode, isBatch);
+ writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error",
+ rootNode.get("id"), false);
return;
}
if (bufferedResp.isOverflow()) {
- JsonNode idNode = isBatch ? null : rootNode.get("id");
writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE,
- "Response exceeds the limit of " + parameter.getJsonRpcMaxResponseSize() + " bytes",
- idNode, isBatch);
+ "Response exceeds the limit of " + maxResponseSize + " bytes",
+ rootNode.get("id"), false);
return;
}
bufferedResp.commitToResponse();
}
+ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxResponseSize)
+ throws IOException {
+
+ ArrayNode batchResult = MAPPER.createArrayNode();
+ int accumulatedSize = 2; // "[]"
+
+ for (int i = 0; i < rootNode.size(); i++) {
+ byte[] subBody;
+ try {
+ subBody = MAPPER.writeValueAsBytes(rootNode.get(i));
+ } catch (JsonProcessingException e) {
+ writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true);
+ return;
+ }
+
+ ByteArrayOutputStream subOutput = new ByteArrayOutputStream();
+ try {
+ rpcServer.handleRequest(new ByteArrayInputStream(subBody), subOutput);
+ } catch (RuntimeException e) {
+ logger.error("RPC execution failed for batch sub-request {}", i, e);
+ writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true);
+ return;
+ }
+
+ byte[] responseBytes = subOutput.toByteArray();
+ if (responseBytes.length == 0) {
+ continue; // notification — no response
+ }
+
+ // comma separator between array elements
+ int addition = responseBytes.length + (!batchResult.isEmpty() ? 1 : 0);
+ if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) {
+ writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE,
+ "Response exceeds the limit of " + maxResponseSize + " bytes", null, true);
+ return;
+ }
+ accumulatedSize += addition;
+
+ JsonNode responseNode;
+ try {
+ responseNode = MAPPER.readTree(responseBytes);
+ } catch (IOException e) {
+ writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true);
+ return;
+ }
+ batchResult.add(responseNode);
+ }
+
+ byte[] finalBytes = MAPPER.writeValueAsBytes(batchResult);
+ resp.setContentType("application/json; charset=utf-8");
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentLength(finalBytes.length);
+ resp.getOutputStream().write(finalBytes);
+ resp.getOutputStream().flush();
+ }
+
private byte[] readBody(InputStream in) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] tmp = new byte[4096];
diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
index 4c60b520752..56cc879f045 100644
--- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
+++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
@@ -13,6 +13,8 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.googlecode.jsonrpc4j.JsonRpcServer;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletRequest;
@@ -87,16 +89,20 @@ public void batchExceedsLimit_returnsExceedLimitAsArray() throws Exception {
@Test
public void batchWithinLimit_proceedsToRpcServer() throws Exception {
CommonParameter.getInstance().jsonRpcMaxBatchSize = 5;
- byte[] rpcResp = "[{\"result\":\"ok\"}]".getBytes(StandardCharsets.UTF_8);
+ byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}"
+ .getBytes(StandardCharsets.UTF_8);
doAnswer(inv -> {
- HttpServletResponse r = inv.getArgument(1);
- r.getOutputStream().write(rpcResp);
- return null;
- }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+ OutputStream out = inv.getArgument(1);
+ out.write(singleResp);
+ return 0;
+ }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class));
MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2}]");
assertEquals(200, resp.getStatus());
- assertArrayEquals(rpcResp, resp.getContentAsByteArray());
+ JsonNode body = MAPPER.readTree(resp.getContentAsByteArray());
+ assertTrue("batch response must be a JSON array", body.isArray());
+ assertEquals("each sub-request must produce a response", 2, body.size());
+ assertEquals("ok", body.get(0).get("result").asText());
}
@Test
@@ -113,12 +119,9 @@ public void emptyBatch_returnsInvalidRequest() throws Exception {
@Test
public void batchLimitDisabled_largeBatchAllowed() throws Exception {
CommonParameter.getInstance().jsonRpcMaxBatchSize = 0;
- byte[] rpcResp = "[]".getBytes(StandardCharsets.UTF_8);
- doAnswer(inv -> {
- HttpServletResponse r = inv.getArgument(1);
- r.getOutputStream().write(rpcResp);
- return null;
- }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+ // write nothing — simulates notifications (no response expected)
+ doAnswer(inv -> 0).when(mockRpcServer)
+ .handleRequest(any(InputStream.class), any(OutputStream.class));
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < 500; i++) {
@@ -130,7 +133,9 @@ public void batchLimitDisabled_largeBatchAllowed() throws Exception {
sb.append("]");
MockHttpServletResponse resp = doPost(sb.toString());
assertEquals(200, resp.getStatus());
- assertArrayEquals(rpcResp, resp.getContentAsByteArray());
+ JsonNode body = MAPPER.readTree(resp.getContentAsByteArray());
+ assertTrue("response must be a JSON array", body.isArray());
+ assertEquals("all notifications produce no response entries", 0, body.size());
}
// --- rpcServer.handle exceptions ---
@@ -149,7 +154,7 @@ public void rpcServerThrowsRuntimeException_returnsInternalError() throws Except
@Test
public void batchRpcServerThrows_internalErrorIsArray() throws Exception {
doThrow(new RuntimeException("boom")).when(mockRpcServer)
- .handle((HttpServletRequest) any(), any());
+ .handleRequest(any(InputStream.class), any(OutputStream.class));
MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]");
assertEquals(200, resp.getStatus());
JsonNode body = MAPPER.readTree(resp.getContentAsString());
@@ -181,10 +186,10 @@ public void batchResponseTooLarge_returnsErrorArray() throws Exception {
int limit = 50;
CommonParameter.getInstance().jsonRpcMaxResponseSize = limit;
doAnswer(inv -> {
- HttpServletResponse r = inv.getArgument(1);
- r.getOutputStream().write(new byte[limit + 1]);
- return null;
- }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+ OutputStream out = inv.getArgument(1);
+ out.write(new byte[limit + 1]);
+ return 0;
+ }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class));
MockHttpServletResponse resp = doPost("[{\"method\":\"eth_getLogs\"}]");
assertEquals(200, resp.getStatus());
@@ -193,6 +198,30 @@ public void batchResponseTooLarge_returnsErrorArray() throws Exception {
assertEquals(-32003, body.get(0).get("error").get("code").asInt());
}
+ @Test
+ public void batchShortCircuitsOnOverflow() throws Exception {
+ int limit = 50;
+ CommonParameter.getInstance().jsonRpcMaxResponseSize = limit;
+ int[] callCount = {0};
+ doAnswer(inv -> {
+ OutputStream out = inv.getArgument(1);
+ callCount[0]++;
+ if (callCount[0] == 1) {
+ out.write("{\"result\":\"ok\"}".getBytes(StandardCharsets.UTF_8));
+ } else {
+ out.write(new byte[limit]); // triggers overflow when added to accumulated size
+ }
+ return 0;
+ }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class));
+
+ MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2},{\"id\":3}]");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertTrue("overflow response must be an array", body.isArray());
+ assertEquals(-32003, body.get(0).get("error").get("code").asInt());
+ assertEquals("third sub-request must not be executed after overflow", 2, callCount[0]);
+ }
+
// --- normal path ---
@Test
+ *
+ *
+ *