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 addr = new ArrayList<>(); int i = 0; for (Object s : (ArrayList) fr.getAddress()) { diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 41e1bbe43e6..c8b22cfd147 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -369,7 +369,8 @@ node { # The maximum blocks range to retrieve logs for eth_getLogs, default value is 5000, # should be > 0, otherwise means no limit. maxBlockRange = 5000 - + # Allowed max address count in filter request + maxAddressSize = 1000 # The maximum number of allowed topics within a topic criteria, default value is 1000, # should be > 0, otherwise means no limit. maxSubTopics = 1000 @@ -378,7 +379,9 @@ node { # Allowed batch size maxBatchSize = 1 # Allowed max response byte size - maxResponseSize = 25 * 1024 * 1024 + 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 eff49b91355883fe2ddbc9a4db66f51d8453e024 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Wed, 1 Apr 2026 21:03:59 +0800 Subject: [PATCH 03/22] update comment --- framework/src/main/resources/config.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index c8b22cfd147..dcd55698fc4 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -369,7 +369,8 @@ node { # The maximum blocks range to retrieve logs for eth_getLogs, default value is 5000, # should be > 0, otherwise means no limit. maxBlockRange = 5000 - # Allowed max address count in filter request + # Allowed max address count in filter request, default value is 1000, + # should be > 0, otherwise means 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. From 19217b8605ceb6deddb05e6b310bff7dfe0f429e Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Thu, 16 Apr 2026 17:50:56 +0800 Subject: [PATCH 04/22] add jsonRpcMaxBatchSize default as 10 --- .../main/java/org/tron/common/parameter/CommonParameter.java | 2 +- framework/src/main/resources/config.conf | 2 +- 2 files changed, 2 insertions(+), 2 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 9fcf8debd66..c995151248a 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 = 1; + public int jsonRpcMaxBatchSize = 10; @Getter @Setter public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index dcd55698fc4..a615361d5f8 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 = 1 + maxBatchSize = 10 # Allowed max response byte size maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B # Allowed max request processing time in seconds From 0351c22ccd93e188ee6536941873b9f47cb21f73 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Mon, 20 Apr 2026 00:00:18 +0800 Subject: [PATCH 05/22] remove timeout restrict --- .../common/parameter/CommonParameter.java | 3 - .../org/tron/core/config/args/NodeConfig.java | 1 - common/src/main/resources/reference.conf | 3 - .../java/org/tron/core/config/args/Args.java | 1 - .../filter/BufferedResponseWrapper.java | 33 +++++++- .../core/services/jsonrpc/JsonRpcServlet.java | 83 +++++++------------ framework/src/main/resources/config.conf | 2 - 7 files changed, 57 insertions(+), 69 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 c995151248a..db14ba84788 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -467,9 +467,6 @@ public class CommonParameter { public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; @Getter @Setter - public int jsonRpcMaxRequestTimeout = 30; - @Getter - @Setter public int jsonRpcMaxAddressSize = 1000; @Getter 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 ab6315c1bbd..72a4fdb5594 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,7 +304,6 @@ 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; } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 2764f37e074..b33e872b68e 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -407,9 +407,6 @@ 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 } 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 e1c57dc7a94..1094b04f2de 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,7 +587,6 @@ 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 ---- 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 fefea565c3c..b603d27032b 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 @@ -1,6 +1,7 @@ package org.tron.core.services.filter; import java.io.ByteArrayOutputStream; +import java.io.IOException; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; @@ -14,11 +15,18 @@ *

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 atLimit = new ArrayList<>(); + atLimit.add(addr1); + atLimit.add(addr2); + FilterRequest frAtLimit = new FilterRequest(); + frAtLimit.setAddress(atLimit); + try { + new LogFilter(frAtLimit); + } catch (JsonRpcInvalidParamsException e) { + Assert.fail("address list at limit should not throw: " + e.getMessage()); + } + + // One over limit — must throw with expected message + ArrayList overLimit = new ArrayList<>(); + overLimit.add(addr1); + overLimit.add(addr2); + overLimit.add(addr3); + FilterRequest frOverLimit = new FilterRequest(); + frOverLimit.setAddress(overLimit); + try { + new LogFilter(frOverLimit); + Assert.fail("address list over limit should have thrown JsonRpcInvalidParamsException"); + } catch (JsonRpcInvalidParamsException e) { + Assert.assertTrue(e.getMessage().contains("exceed max addresses:")); + } + + // Limit = 0 means disabled — large list must pass + CommonParameter.getInstance().jsonRpcMaxAddressSize = 0; + ArrayList largeList = new ArrayList<>(Collections.nCopies(500, addr1)); + FilterRequest frDisabled = new FilterRequest(); + frDisabled.setAddress(largeList); + try { + new LogFilter(frDisabled); + } catch (JsonRpcInvalidParamsException e) { + Assert.fail("limit=0 should disable the check: " + e.getMessage()); + } + } finally { + CommonParameter.getInstance().jsonRpcMaxAddressSize = savedLimit; + } + } + private int[] getBloomIndex(String s) { Bloom bloom = Bloom.create(Hash.sha3(ByteArray.fromHexString(s))); BitSet bs = BitSet.valueOf(bloom.getData()); 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 6a6c679070f..4c60b520752 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 @@ -99,6 +99,17 @@ public void batchWithinLimit_proceedsToRpcServer() throws Exception { assertArrayEquals(rpcResp, resp.getContentAsByteArray()); } + @Test + public void emptyBatch_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("[]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertFalse("empty-batch error response must be a single object, not an array", body.isArray()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertEquals("2.0", body.get("jsonrpc").asText()); + assertTrue(body.get("id").isNull()); + } + @Test public void batchLimitDisabled_largeBatchAllowed() throws Exception { CommonParameter.getInstance().jsonRpcMaxBatchSize = 0; From fd7fdf9cc68a9a184ccc09dc9a9b1b882bcf1ac3 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Fri, 8 May 2026 15:50:00 +0800 Subject: [PATCH 19/22] reorganize the package import --- framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 598af6f8d3a..5f577194dff 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java @@ -11,11 +11,11 @@ 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; import org.tron.common.crypto.Hash; +import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; From 2fdb6b8af848d965acc2c7718fea709e749d2c15 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Fri, 8 May 2026 22:20:49 +0800 Subject: [PATCH 20/22] add some document for CachedBodyRequestWrapper --- .../filter/CachedBodyRequestWrapper.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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 ef33f92d25a..ce4278ddecb 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 @@ -13,11 +13,27 @@ import javax.servlet.http.HttpServletRequestWrapper; /** - * Wraps a request and replays a pre-read body from a byte array. + * Wraps a request to replay a pre-read body from a byte array, + * allowing the body to be read more than once. + * + *

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