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 0028a5d50d0..c9ce47d477b 100644
--- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java
+++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java
@@ -483,6 +483,15 @@ public class CommonParameter {
@Getter
@Setter
public int jsonRpcMaxBlockFilterNum = 50000;
+ @Getter
+ @Setter
+ public int jsonRpcMaxBatchSize = 100;
+ @Getter
+ @Setter
+ public int jsonRpcMaxResponseSize = 25 * 1024 * 1024;
+ @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 620152a907a..7dd3551a12d 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
@@ -310,6 +310,9 @@ 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;
+ private int maxAddressSize = 1000;
private long maxMessageSize = 4194304;
}
diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf
index 63e5d86a4af..e5c5e90c6c4 100644
--- a/common/src/main/resources/reference.conf
+++ b/common/src/main/resources/reference.conf
@@ -396,6 +396,15 @@ 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), >0 otherwise no limit
+ maxResponseSize = 26214400
+
+ # Maximum number of addresses in a single JSON-RPC request, >0 otherwise no limit
+ maxAddressSize = 1000
+
# Maximum JSON-RPC request body size, default 4MB. Independent from rpc.maxMessageSize.
maxMessageSize = 4M
}
diff --git a/framework/build.gradle b/framework/build.gradle
index 7b3e6ddb968..ec89e99f1c8 100644
--- a/framework/build.gradle
+++ b/framework/build.gradle
@@ -56,6 +56,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/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java
index 652f37a90db..e29c139ab92 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
@@ -564,6 +564,9 @@ 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();
+ PARAMETER.jsonRpcMaxAddressSize = jsonrpc.getMaxAddressSize();
PARAMETER.jsonRpcMaxMessageSize = jsonrpc.getMaxMessageSize();
// ---- 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
new file mode 100644
index 00000000000..e99dcffde67
--- /dev/null
+++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
@@ -0,0 +1,171 @@
+package org.tron.core.services.filter;
+
+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;
+import javax.servlet.http.HttpServletResponseWrapper;
+import lombok.Getter;
+
+/**
+ * Buffers the response body without writing to the underlying response,
+ * so the caller can replay it after the handler returns.
+ *
+ *
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()}.
+ */
+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 boolean committed = false;
+ @Getter
+ private volatile boolean overflow = false;
+
+ private final ServletOutputStream outputStream = new ServletOutputStream() {
+ @Override
+ public void write(int b) {
+ if (overflow) {
+ return;
+ }
+ if (maxBytes > 0 && buffer.size() >= maxBytes) {
+ markOverflow();
+ return;
+ }
+ buffer.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) {
+ if (overflow) {
+ return;
+ }
+ if (maxBytes > 0 && buffer.size() + len > maxBytes) {
+ markOverflow();
+ return;
+ }
+ buffer.write(b, off, len);
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+ }
+ };
+
+ private final PrintWriter writer =
+ new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8), true);
+
+ /**
+ * @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.actual = response;
+ this.maxBytes = 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();
+ }
+ }
+
+ @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;
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() {
+ return outputStream;
+ }
+
+ @Override
+ public PrintWriter getWriter() {
+ return writer;
+ }
+
+ public void commitToResponse() throws IOException {
+ if (committed) {
+ throw new IllegalStateException("commitToResponse() already called");
+ }
+ committed = true;
+ if (contentType != null) {
+ actual.setContentType(contentType);
+ }
+ actual.setStatus(status);
+ actual.setContentLength(buffer.size());
+ buffer.writeTo(actual.getOutputStream());
+ actual.getOutputStream().flush();
+ }
+}
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..683fe849f71
--- /dev/null
+++ b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java
@@ -0,0 +1,97 @@
+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.IllegalCharsetNameException;
+import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+
+/**
+ * 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:
+ *
+ * - {@code application/x-www-form-urlencoded} — cached body cannot back
+ * {@code getParameter*}.
+ * - multipart — {@code getPart()/getParts()} read from the original
+ * (already-consumed) stream.
+ * - async non-blocking I/O — see {@code setReadListener}.
+ * - request dispatch / forward chains.
+ *
+ *
+ * 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 final byte[] body;
+ private BodyAccessor accessor = BodyAccessor.NONE;
+
+ public CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) {
+ super(request);
+ this.body = 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
+ 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) {
+ throw new UnsupportedOperationException(
+ "async I/O is not supported on cached body");
+ }
+ };
+ }
+
+ @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;
+ 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 104a0e9e470..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
@@ -1,10 +1,18 @@
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;
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;
import java.util.Collections;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
@@ -14,15 +22,30 @@
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 enum JsonRpcError {
+ PARSE_ERROR(-32700),
+ INVALID_REQUEST(-32600),
+ INTERNAL_ERROR(-32603),
+ EXCEED_LIMIT(-32005),
+ RESPONSE_TOO_LARGE(-32003);
+
+ private final int code;
+
+ JsonRpcError(int code) {
+ this.code = code;
+ }
+ }
+
private JsonRpcServer rpcServer = null;
@Autowired
@@ -66,6 +89,156 @@ public Integer getJsonRpcCode(int httpStatusCode) {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
- rpcServer.handle(req, resp);
+ CommonParameter parameter = CommonParameter.getInstance();
+
+ // Transport IOException from readBody propagates as HTTP 500 (genuine IO failure).
+ byte[] body = readBody(req.getInputStream());
+ JsonNode rootNode;
+ try {
+ rootNode = MAPPER.readTree(body);
+ 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();
+ 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,
+ "Batch size " + rootNode.size() + " exceeds the limit of " + batchSize, null, true);
+ 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, maxResponseSize);
+
+ try {
+ rpcServer.handle(cachedReq, bufferedResp);
+ } catch (RuntimeException e) {
+ logger.error("RPC execution failed", e);
+ writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error",
+ rootNode.get("id"), false);
+ return;
+ }
+
+ if (bufferedResp.isOverflow()) {
+ writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE,
+ "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];
+ int n;
+ while ((n = in.read(tmp)) != -1) {
+ buffer.write(tmp, 0, n);
+ }
+ return buffer.toByteArray();
+ }
+
+ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, String message,
+ 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()) {
+ errorObj.set("id", id);
+ } else {
+ errorObj.putNull("id");
+ }
+ byte[] bytes;
+ if (isBatch) {
+ ArrayNode arr = MAPPER.createArrayNode();
+ arr.add(errorObj);
+ bytes = MAPPER.writeValueAsBytes(arr);
+ } else {
+ bytes = MAPPER.writeValueAsBytes(errorObj);
+ }
+ resp.setContentType("application/json; charset=utf-8");
+ 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/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 6c8f2082301..5f64a47e7d3 100644
--- a/framework/src/main/resources/config.conf
+++ b/framework/src/main/resources/config.conf
@@ -384,15 +384,18 @@ 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 means no limit
maxBlockRange = 5000
-
- # The maximum number of allowed topics within a topic criteria, default value is 1000,
- # should be > 0, otherwise means 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 means no limit
maxSubTopics = 1000
- # Allowed maximum number for blockFilter
+ # Allowed maximum number for blockFilter, default: 50000, <=0 means no limit
maxBlockFilterNum = 50000
+ # Allowed batch size, default: 100, <=0 means no limit
+ maxBatchSize = 100
+ # Allowed max response byte size, default: 26214400 (25 MB), <=0 means no limit
+ maxResponseSize = 26214400
}
# Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode,
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..5f577194dff 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java
@@ -8,12 +8,14 @@
import java.util.ArrayList;
import java.util.BitSet;
+import java.util.Collections;
import java.util.List;
import org.bouncycastle.util.encoders.Hex;
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;
@@ -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/filter/BufferedResponseWrapperTest.java b/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java
new file mode 100644
index 00000000000..76de2d2db2e
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java
@@ -0,0 +1,264 @@
+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.assertNull;
+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());
+ }
+
+ // --- 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..813b1a61bea
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java
@@ -0,0 +1,109 @@
+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..56cc879f045
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
@@ -0,0 +1,257 @@
+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.io.InputStream;
+import java.io.OutputStream;
+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[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}"
+ .getBytes(StandardCharsets.UTF_8);
+ doAnswer(inv -> {
+ 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());
+ 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
+ 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;
+ // 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++) {
+ if (i > 0) {
+ sb.append(',');
+ }
+ sb.append("{}");
+ }
+ sb.append("]");
+ MockHttpServletResponse resp = doPost(sb.toString());
+ assertEquals(200, resp.getStatus());
+ 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 ---
+
+ @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)
+ .handleRequest(any(InputStream.class), any(OutputStream.class));
+ 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 -> {
+ 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());
+ 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());
+ }
+
+ @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
+ 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);
+ }
+ }
+}