diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java
index 7060f1f135a..a06e09c6a25 100644
--- a/framework/src/main/java/org/tron/common/application/HttpService.java
+++ b/framework/src/main/java/org/tron/common/application/HttpService.java
@@ -30,7 +30,6 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ErrorHandler;
-import org.eclipse.jetty.server.handler.SizeLimitHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.tron.core.config.args.Args;
@@ -110,7 +109,8 @@ protected void initServer() {
protected ServletContextHandler initContextHandler() {
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath(this.contextPath);
- SizeLimitHandler sizeLimitHandler = new SizeLimitHandler(this.maxRequestSize, -1);
+ RequestSizeLimitHandler sizeLimitHandler =
+ new RequestSizeLimitHandler(this.maxRequestSize);
sizeLimitHandler.setHandler(context);
this.apiServer.setHandler(sizeLimitHandler);
return context;
@@ -123,7 +123,7 @@ protected void addFilter(ServletContextHandler context) {
}
/**
- * For oversized requests (the 413 thrown by SizeLimitHandler during dispatch) logs the
+ * For oversized requests (the 413 thrown by RequestSizeLimitHandler during dispatch) logs the
* detail server-side and returns the short, uniform bad-message page, instead of the
* default error page that leaks the exception stack and internal request sizes. All
* other errors keep Jetty's default handling.
diff --git a/framework/src/main/java/org/tron/common/application/RequestSizeLimitHandler.java b/framework/src/main/java/org/tron/common/application/RequestSizeLimitHandler.java
new file mode 100644
index 00000000000..a92aa33c3b5
--- /dev/null
+++ b/framework/src/main/java/org/tron/common/application/RequestSizeLimitHandler.java
@@ -0,0 +1,55 @@
+package org.tron.common.application;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.server.HttpInput;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+
+/**
+ * Enforces request-body limits using Jetty's received-byte counter.
+ *
+ *
This request-only handler installs its own input interceptor to avoid counting the same
+ * unread content repeatedly. It does not install a response-limiting output interceptor.
+ */
+public class RequestSizeLimitHandler extends HandlerWrapper {
+
+ private final long requestLimit;
+
+ public RequestSizeLimitHandler(long requestLimit) {
+ this.requestLimit = requestLimit;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request,
+ HttpServletResponse response) throws IOException, ServletException {
+ if (requestLimit >= 0) {
+ long contentLength = baseRequest.getContentLengthLong();
+ checkRequestLimit(contentLength);
+
+ if (contentLength < 0) {
+ HttpInput input = baseRequest.getHttpInput();
+ input.addInterceptor(content -> {
+ if (content == null) {
+ return null;
+ }
+ // addContent updates this counter before interception, under HttpInput's input lock.
+ checkRequestLimit(input.getContentReceived());
+ return content;
+ });
+ }
+ }
+
+ super.handle(target, baseRequest, request, response);
+ }
+
+ private void checkRequestLimit(long size) {
+ if (requestLimit >= 0 && size > requestLimit) {
+ throw new BadMessageException(413,
+ "Request body is too large: " + size + ">" + requestLimit);
+ }
+ }
+}
diff --git a/framework/src/test/java/org/tron/common/jetty/RequestSizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/RequestSizeLimitHandlerTest.java
new file mode 100644
index 00000000000..8e4b098ede0
--- /dev/null
+++ b/framework/src/test/java/org/tron/common/jetty/RequestSizeLimitHandlerTest.java
@@ -0,0 +1,283 @@
+package org.tron.common.jetty;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.server.HttpInput;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.tron.common.application.RequestSizeLimitHandler;
+import org.tron.json.JSONObject;
+
+public class RequestSizeLimitHandlerTest {
+
+ private static final int LIMIT = 64 * 1024;
+ private final Map inputs = new ConcurrentHashMap<>();
+ private final Map> arrivals = new ConcurrentHashMap<>();
+ private Server server;
+ private int port;
+ private volatile CountDownLatch firstReads;
+
+ @Before
+ public void setup() throws Exception {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setHost("127.0.0.1");
+ connector.setPort(0);
+ server.addConnector(connector);
+ RequestSizeLimitHandler limitHandler = new RequestSizeLimitHandler(LIMIT);
+ limitHandler.setHandler(new AbstractHandler() {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request,
+ HttpServletResponse response) throws IOException {
+ HttpInput input = baseRequest.getHttpInput();
+ inputs.put(target, input);
+ int count = 0;
+ int error = 0;
+ long firstReceived = 0;
+ try {
+ while (input.read() != -1) {
+ count++;
+ int checkpoint = firstReads == null ? 1
+ : 13 + Integer.parseInt(target.substring("/concurrent".length()));
+ if (count == checkpoint) {
+ firstReceived = input.getContentReceived();
+ if (firstReads != null) {
+ firstReads.countDown();
+ }
+ }
+ }
+ } catch (BadMessageException e) {
+ error = e.getCode();
+ }
+ byte[] body = ("{\"bytes\":" + count + ",\"received\":" + input.getContentReceived()
+ + ",\"firstReceived\":" + firstReceived + ",\"error\":" + error + "}")
+ .getBytes(StandardCharsets.UTF_8);
+ // Diagnostic endpoint: assertions inspect the caught exception code and read count.
+ response.setStatus(200);
+ response.setContentType("application/json");
+ response.setContentLength(body.length);
+ response.getOutputStream().write(body);
+ baseRequest.setHandled(true);
+ }
+ });
+ HandlerWrapper observer = new HandlerWrapper() {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request,
+ HttpServletResponse response) throws IOException, ServletException {
+ HttpInput input = baseRequest.getHttpInput();
+ List counts = Collections.synchronizedList(new ArrayList<>());
+ arrivals.put(target, counts);
+ input.addInterceptor(content -> {
+ if (content != null && content.hasContent()) {
+ long received = input.getContentReceived();
+ if (counts.isEmpty() || counts.get(counts.size() - 1) != received) {
+ counts.add(received);
+ }
+ }
+ return content;
+ });
+ super.handle(target, baseRequest, request, response);
+ }
+ };
+ observer.setHandler(limitHandler);
+ server.setHandler(observer);
+ server.start();
+ port = connector.getLocalPort();
+ }
+
+ @After
+ public void teardown() throws Exception {
+ server.stop();
+ }
+
+ @Test
+ public void testOneByteReadsAcceptExactLimit() throws Exception {
+ try (Socket socket = connect()) {
+ writeHeaders(socket, "/exact", true, LIMIT);
+ writeBody(socket, true, LIMIT);
+ JSONObject result = readResponse(socket);
+ Assert.assertEquals(0, result.getIntValue("error"));
+ Assert.assertEquals(LIMIT, result.getIntValue("bytes"));
+ Assert.assertEquals(LIMIT, result.getLongValue("received"));
+ }
+ }
+
+ @Test
+ public void testOneByteReadsRejectBeforeReturningBytesBeyondLimit() throws Exception {
+ try (Socket socket = connect()) {
+ writeHeaders(socket, "/over", true, LIMIT + 4096);
+ writeBody(socket, true, LIMIT + 4096);
+ JSONObject result = readResponse(socket);
+ Assert.assertEquals(413, result.getIntValue("error"));
+ Assert.assertTrue(result.toJSONString(), result.getIntValue("bytes") <= LIMIT);
+ }
+ }
+
+ @Test
+ public void testConcurrentRequestsHaveIndependentCumulativeCounters() throws Exception {
+ int requests = 4;
+ firstReads = new CountDownLatch(requests);
+ ExecutorService executor = Executors.newFixedThreadPool(requests,
+ new ThreadFactoryBuilder().setNameFormat("request-size-counter-client-%d").build());
+ try {
+ List> results = new ArrayList<>();
+ for (int i = 0; i < requests; i++) {
+ final int index = i;
+ results.add(executor.submit(() -> {
+ boolean chunked = index % 2 == 0;
+ int total = 1024 + index * 257;
+ int first = 13 + index;
+ try (Socket socket = connect()) {
+ writeHeaders(socket, "/concurrent" + index, chunked, total);
+ writePart(socket, chunked, first);
+ // All four bodies are partially read before any client sends its remainder.
+ Assert.assertTrue(firstReads.await(10, TimeUnit.SECONDS));
+ writePart(socket, chunked, 31);
+ writePart(socket, chunked, total - first - 31);
+ finishBody(socket, chunked);
+ return readResponse(socket);
+ }
+ }));
+ }
+ for (int i = 0; i < requests; i++) {
+ JSONObject result = results.get(i).get(15, TimeUnit.SECONDS);
+ int total = 1024 + i * 257;
+ Assert.assertEquals(0, result.getIntValue("error"));
+ Assert.assertEquals(total, result.getIntValue("bytes"));
+ Assert.assertEquals(total, result.getLongValue("received"));
+ Assert.assertEquals(13 + i, result.getLongValue("firstReceived"));
+ List counts = arrivals.get("/concurrent" + i);
+ Assert.assertTrue(counts.toString(), counts.contains((long) (13 + i)));
+ Assert.assertEquals(total, counts.get(counts.size() - 1).longValue());
+ for (int j = 1; j < counts.size(); j++) {
+ Assert.assertTrue(counts.toString(), counts.get(j) > counts.get(j - 1));
+ }
+ for (int j = 0; j < i; j++) {
+ Assert.assertNotSame(inputs.get("/concurrent" + i), inputs.get("/concurrent" + j));
+ }
+ }
+ } finally {
+ executor.shutdown();
+ if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
+ executor.shutdownNow();
+ Assert.assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
+ }
+ }
+ }
+
+ @Test
+ public void testKeepAliveRecyclesCounterBetweenFramings() throws Exception {
+ try (Socket socket = connect()) {
+ for (int i = 0; i < 4; i++) {
+ boolean chunked = i % 2 == 0;
+ int total = 1024 - i * 257;
+ writeHeaders(socket, "/reuse" + i, chunked, total);
+ writeBody(socket, chunked, total);
+ JSONObject result = readResponse(socket);
+ Assert.assertEquals(0, result.getIntValue("error"));
+ Assert.assertEquals(total, result.getIntValue("bytes"));
+ Assert.assertEquals(total, result.getLongValue("received"));
+ Assert.assertSame(inputs.get("/reuse0"), inputs.get("/reuse" + i));
+ }
+ }
+ }
+
+ private Socket connect() throws Exception {
+ Socket socket = new Socket("127.0.0.1", port);
+ socket.setSoTimeout(15000);
+ return socket;
+ }
+
+ private void writeHeaders(Socket socket, String path, boolean chunked, int total)
+ throws Exception {
+ String framing = chunked ? "Transfer-Encoding: chunked" : "Content-Length: " + total;
+ socket.getOutputStream().write(("POST " + path + " HTTP/1.1\r\nHost: localhost\r\n"
+ + framing + "\r\n\r\n").getBytes(StandardCharsets.US_ASCII));
+ socket.getOutputStream().flush();
+ }
+
+ private void writeBody(Socket socket, boolean chunked, int total) throws Exception {
+ writePart(socket, chunked, total);
+ finishBody(socket, chunked);
+ }
+
+ private void writePart(Socket socket, boolean chunked, int size) throws Exception {
+ OutputStream output = socket.getOutputStream();
+ if (chunked) {
+ output.write((Integer.toHexString(size) + "\r\n").getBytes(StandardCharsets.US_ASCII));
+ }
+ byte[] body = new byte[size];
+ Arrays.fill(body, (byte) 'a');
+ output.write(body);
+ if (chunked) {
+ output.write("\r\n".getBytes(StandardCharsets.US_ASCII));
+ }
+ output.flush();
+ }
+
+ private void finishBody(Socket socket, boolean chunked) throws Exception {
+ if (chunked) {
+ socket.getOutputStream().write("0\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
+ socket.getOutputStream().flush();
+ }
+ }
+
+ private JSONObject readResponse(Socket socket) throws Exception {
+ InputStream input = socket.getInputStream();
+ Assert.assertTrue(readLine(input).startsWith("HTTP/1.1 200"));
+ int length = -1;
+ String line;
+ while (!(line = readLine(input)).isEmpty()) {
+ if (line.startsWith("Content-Length:")) {
+ length = Integer.parseInt(line.substring("Content-Length:".length()).trim());
+ }
+ }
+ Assert.assertTrue("response must have a content length", length >= 0);
+ byte[] body = new byte[length];
+ for (int i = 0; i < length; i++) {
+ int value = input.read();
+ Assert.assertTrue("early response EOF", value >= 0);
+ body[i] = (byte) value;
+ }
+ return JSONObject.parseObject(new String(body, StandardCharsets.UTF_8));
+ }
+
+ private String readLine(InputStream input) throws Exception {
+ ByteArrayOutputStream line = new ByteArrayOutputStream();
+ int value;
+ while ((value = input.read()) != '\n') {
+ Assert.assertTrue("early header EOF", value >= 0);
+ if (value != '\r') {
+ line.write(value);
+ }
+ }
+ return new String(line.toByteArray(), StandardCharsets.US_ASCII);
+ }
+}
diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java
index 145eda6d398..295eee2cf6b 100644
--- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java
+++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java
@@ -1,26 +1,40 @@
package org.tron.common.jetty;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
+import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
+import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.AfterClass;
@@ -29,15 +43,21 @@
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
+import org.springframework.test.util.ReflectionTestUtils;
import org.tron.common.TestConstants;
import org.tron.common.application.HttpService;
+import org.tron.common.application.RequestSizeLimitHandler;
import org.tron.common.utils.PublicMethod;
+import org.tron.core.Wallet;
import org.tron.core.config.args.Args;
+import org.tron.core.services.http.servlets.GetBlockByNumServlet;
+import org.tron.core.services.ratelimiter.RateLimiterContainer;
import org.tron.json.JSONObject;
+import org.tron.protos.Protocol.Block;
/**
- * Tests {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size
- * enforcement configured in {@link HttpService}.
+ * Tests {@link RequestSizeLimitHandler} body-size enforcement configured in
+ * {@link HttpService}.
*
* Covers: accept/reject by size, UTF-8 byte counting, independent limits
* across HttpService instances, chunked transfer, and zero-limit behavior.
@@ -50,6 +70,7 @@ public class SizeLimitHandlerTest {
private static final int HTTP_MAX_BODY_SIZE = 1024;
private static final int SECOND_SERVICE_MAX_BODY_SIZE = 512;
+ private static final int CLIENT_TIMEOUT_MILLIS = 15000;
@ClassRule
public static final TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -78,7 +99,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
// Mimics RateLimiterServlet line 119-120: silently logs, does not rethrow
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentType("application/json");
- resp.getWriter().println("{\"Error\":\"" + e.getClass().getSimpleName() + "\"}");
+ resp.getWriter().println("{\"Error\":\"" + e.getClass().getSimpleName()
+ + "\",\"detail\":\"" + e.getMessage() + "\"}");
}
}
}
@@ -97,6 +119,37 @@ protected void addServlet(ServletContextHandler context) {
}
}
+ static class ObserverHttpService extends TestHttpService {
+ private final AtomicInteger interceptions;
+
+ ObserverHttpService(int port, long maxRequestSize, AtomicInteger interceptions) {
+ super(port, maxRequestSize);
+ this.interceptions = interceptions;
+ }
+
+ @Override
+ protected ServletContextHandler initContextHandler() {
+ ServletContextHandler context = super.initContextHandler();
+ HandlerWrapper limitHandler = (HandlerWrapper) apiServer.getHandler();
+ HandlerWrapper observer = new HandlerWrapper() {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request,
+ HttpServletResponse response) throws IOException, ServletException {
+ baseRequest.getHttpInput().addInterceptor(content -> {
+ if (content != null) {
+ interceptions.incrementAndGet();
+ }
+ return content;
+ });
+ super.handle(target, baseRequest, request, response);
+ }
+ };
+ observer.setHandler(limitHandler);
+ apiServer.setHandler(observer);
+ return context;
+ }
+ }
+
/** Second HttpService instance with a different size limit, for independence tests. */
static class SecondHttpService extends HttpService {
SecondHttpService(int port, long maxRequestSize) {
@@ -128,7 +181,13 @@ public static void setup() throws Exception {
secondService.start().get(10, TimeUnit.SECONDS);
secondServerUri = new URI(String.format("http://localhost:%d/", secondPort));
- client = HttpClients.createDefault();
+ client = HttpClients.custom()
+ .setDefaultRequestConfig(RequestConfig.custom()
+ .setConnectTimeout(CLIENT_TIMEOUT_MILLIS)
+ .setConnectionRequestTimeout(CLIENT_TIMEOUT_MILLIS)
+ .setSocketTimeout(CLIENT_TIMEOUT_MILLIS)
+ .build())
+ .build();
}
@AfterClass
@@ -257,14 +316,137 @@ public void testChunkedBodyWithinLimit() throws Exception {
Assert.assertEquals(200, post(httpServerUri, chunked));
}
+ @Test
+ public void testLargeBodyWithinLimitMatchesContentLengthAndChunked() throws Exception {
+ int limit = 4 * 1024 * 1024;
+ int port = PublicMethod.chooseRandomPort();
+ TestHttpService service = new TestHttpService(port, limit);
+ try {
+ service.start().get(10, TimeUnit.SECONDS);
+ URI uri = new URI(String.format("http://localhost:%d/", port));
+ String body = repeat('a', 3 * 1024 * 1024);
+ byte[] data = body.getBytes("UTF-8");
+
+ String knownLengthResponse = postForBody(uri, new StringEntity(body));
+ String chunkedResponse = postForBody(uri,
+ new InputStreamEntity(new ByteArrayInputStream(data), -1));
+
+ Assert.assertEquals(knownLengthResponse, chunkedResponse);
+ Assert.assertEquals(data.length,
+ JSONObject.parseObject(chunkedResponse).getIntValue("bytes"));
+ } finally {
+ service.stop();
+ }
+ }
+
+ @Test
+ public void testGetBlockByNumAcceptsSameLargeBodyWithBothFramings() throws Exception {
+ int limit = 4 * 1024 * 1024;
+ long originalLimit = Args.getInstance().getHttpMaxMessageSize();
+ boolean originalNonBlocking = Args.getInstance().isRateLimiterApiNonBlocking();
+ Args.getInstance().setHttpMaxMessageSize(limit);
+ Args.getInstance().setRateLimiterApiNonBlocking(false);
+
+ Wallet wallet = mock(Wallet.class);
+ when(wallet.getBlockByNum(0)).thenReturn(Block.getDefaultInstance());
+ GetBlockByNumServlet servlet = new GetBlockByNumServlet();
+ ReflectionTestUtils.setField(servlet, "wallet", wallet);
+ ReflectionTestUtils.setField(servlet, "container", new RateLimiterContainer());
+
+ int port = PublicMethod.chooseRandomPort();
+ TestHttpService service = new TestHttpService(port, limit) {
+ @Override
+ protected void addServlet(ServletContextHandler context) {
+ context.addServlet(new ServletHolder(servlet), "/wallet/getblockbynum");
+ }
+ };
+ try {
+ service.start().get(10, TimeUnit.SECONDS);
+ URI uri = new URI(String.format("http://localhost:%d/wallet/getblockbynum", port));
+ String prefix = "{\"num\":0,\"_pad\":\"";
+ String suffix = "\"}";
+ String body = prefix + repeat('A', 3 * 1024 * 1024 - prefix.length() - suffix.length())
+ + suffix;
+ byte[] data = body.getBytes(StandardCharsets.UTF_8);
+ InputStreamEntity chunked = new InputStreamEntity(new ByteArrayInputStream(data), -1);
+ chunked.setContentType(ContentType.APPLICATION_JSON.getMimeType());
+
+ String knownLengthResponse = postForBody(uri,
+ new StringEntity(body, ContentType.APPLICATION_JSON));
+ String chunkedResponse = postForBody(uri, chunked);
+
+ Assert.assertEquals(knownLengthResponse, chunkedResponse);
+ Assert.assertFalse(chunkedResponse.contains("Error"));
+ verify(wallet, times(2)).getBlockByNum(0);
+ } finally {
+ try {
+ service.stop();
+ } finally {
+ Args.getInstance().setHttpMaxMessageSize(originalLimit);
+ Args.getInstance().setRateLimiterApiNonBlocking(originalNonBlocking);
+ }
+ }
+ }
+
+ @Test
+ public void testChunkedBodyAtLimitWithDifferentChunkSizes() throws Exception {
+ byte[] data = repeat('a', HTTP_MAX_BODY_SIZE).getBytes("UTF-8");
+ for (int chunkSize : new int[] {1, 7, 64, 1024, 4096}) {
+ String response = postChunked(httpServerUri, data, chunkSize);
+ Assert.assertEquals("chunk size " + chunkSize, data.length,
+ JSONObject.parseObject(response).getIntValue("bytes"));
+ }
+ }
+
+ @Test
+ public void testChunkedBodyOneByteOverLimitUsesActualSize() throws Exception {
+ byte[] data = repeat('a', HTTP_MAX_BODY_SIZE + 1).getBytes("UTF-8");
+ String response = postForBody(httpServerUri,
+ new InputStreamEntity(new ByteArrayInputStream(data), -1));
+
+ Assert.assertTrue(response, response.contains("BadMessageException"));
+ String detail = (HTTP_MAX_BODY_SIZE + 1) + ">" + HTTP_MAX_BODY_SIZE;
+ Assert.assertTrue(response, response.contains(detail));
+ }
+
+ @Test
+ public void testChunkedBodyCountResetBetweenRequests() throws Exception {
+ byte[] data = repeat('a', HTTP_MAX_BODY_SIZE).getBytes("UTF-8");
+ for (int i = 0; i < 2; i++) {
+ String response = postForBody(httpServerUri,
+ new InputStreamEntity(new ByteArrayInputStream(data), -1));
+ Assert.assertEquals(data.length, JSONObject.parseObject(response).getIntValue("bytes"));
+ }
+ }
+
+ @Test
+ public void testExistingInterceptorIsPreserved() throws Exception {
+ AtomicInteger interceptions = new AtomicInteger();
+ int port = PublicMethod.chooseRandomPort();
+ ObserverHttpService service = new ObserverHttpService(port, HTTP_MAX_BODY_SIZE,
+ interceptions);
+ try {
+ service.start().get(10, TimeUnit.SECONDS);
+ URI uri = new URI(String.format("http://localhost:%d/", port));
+ byte[] data = repeat('a', HTTP_MAX_BODY_SIZE).getBytes("UTF-8");
+ String response = postForBody(uri,
+ new InputStreamEntity(new ByteArrayInputStream(data), -1));
+
+ Assert.assertEquals(data.length, JSONObject.parseObject(response).getIntValue("bytes"));
+ Assert.assertTrue(interceptions.get() > 0);
+ } finally {
+ service.stop();
+ }
+ }
+
/**
* Chunked oversized body hitting a servlet with broad catch(Exception).
*
- * SizeLimitHandler's LimitInterceptor throws BadMessageException during
- * streaming read, but the servlet's catch(Exception) absorbs it and returns
- * 200 + error JSON instead of 413. This matches real TRON servlet behavior.
+ * RequestSizeLimitHandler throws BadMessageException during streaming read,
+ * but the servlet's catch(Exception) absorbs it and returns 200 + error JSON
+ * instead of 413. This matches real TRON servlet behavior.
*
- * OOM protection still works: the body read is truncated at the limit.
+ * The size check rejects further reading; this does not establish an OOM guarantee.
*/
@Test
public void testChunkedBodyExceedsLimit() throws Exception {
@@ -278,7 +460,7 @@ public void testChunkedBodyExceedsLimit() throws Exception {
logger.info("Chunked oversized: status={}, body={}", status, body);
// catch(Exception) absorbs BadMessageException -> 200 + error JSON, not 413.
- // Body read IS truncated - OOM protection still effective.
+ // Verify the reading failure separately from the public HTTP status.
Assert.assertEquals(200, status);
Assert.assertTrue("Error should be surfaced in response body",
body.contains("Error"));
@@ -308,6 +490,61 @@ public void testZeroLimitRejectsAllBodies() throws Exception {
}
}
+ @Test
+ public void testNegativeLimitDisablesRequestSizeCheck() throws Exception {
+ int port = PublicMethod.chooseRandomPort();
+ TestHttpService service = new TestHttpService(port, -1);
+ try {
+ service.start().get(10, TimeUnit.SECONDS);
+ URI uri = new URI(String.format("http://localhost:%d/", port));
+ String body = repeat('a', HTTP_MAX_BODY_SIZE * 2);
+ byte[] data = body.getBytes("UTF-8");
+
+ String knownLengthResponse = postForBody(uri, new StringEntity(body));
+ String chunkedResponse = postForBody(uri,
+ new InputStreamEntity(new ByteArrayInputStream(data), -1));
+
+ Assert.assertEquals(knownLengthResponse, chunkedResponse);
+ Assert.assertEquals(data.length,
+ JSONObject.parseObject(chunkedResponse).getIntValue("bytes"));
+ } finally {
+ service.stop();
+ }
+ }
+
+ @Test
+ public void testHttp10ContentLengthBoundaries() throws Exception {
+ String withinLimit = "POST / HTTP/1.0\r\nHost: localhost\r\nContent-Length: 3\r\n\r\nabc";
+ String response = sendRaw(httpServerUri, withinLimit);
+ Assert.assertTrue(response, response.startsWith("HTTP/1.1 200"));
+ Assert.assertTrue(response, response.contains("\"bytes\":3"));
+
+ String body = repeat('a', HTTP_MAX_BODY_SIZE + 1);
+ String overLimit = "POST / HTTP/1.0\r\nHost: localhost\r\nContent-Length: "
+ + body.length() + "\r\n\r\n" + body;
+ response = sendRaw(httpServerUri, overLimit);
+ Assert.assertTrue(response, response.startsWith("HTTP/1.1 413"));
+ }
+
+ @Test
+ public void testHttp10ChunkedRequestCompatibility() throws Exception {
+ String request = "POST / HTTP/1.0\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n"
+ + "1\r\na\r\n0\r\n\r\n";
+ String response = sendRaw(httpServerUri, request);
+ Assert.assertTrue(response, response.startsWith("HTTP/1.1 200"));
+ Assert.assertTrue(response, response.contains("\"bytes\":1"));
+ }
+
+ @Test
+ public void testMalformedChunkFailsDuringBodyRead() throws Exception {
+ String request = "POST / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n"
+ + "Transfer-Encoding: chunked\r\n\r\nxyz\r\n";
+ String response = sendRaw(httpServerUri, request);
+ Assert.assertTrue(response, response.startsWith("HTTP/1.1 200"));
+ Assert.assertTrue(response, response.contains("UncheckedIOException"));
+ Assert.assertTrue(response, response.contains("Early EOF"));
+ }
+
/**
* For pure ASCII JSON (the normal TRON API case), wire bytes and
* {@code body.getBytes().length} (what {@code Util.checkBodySize()} measures)
@@ -366,6 +603,33 @@ public void testCheckBodySizeSafeDirectionWithNewlines() throws Exception {
wireBytes, servletBytes, wireBytes - servletBytes);
}
+ /** POSTs with a specific client chunk size and returns the response body. */
+ private String postChunked(URI uri, byte[] data, int chunkSize) throws Exception {
+ HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
+ try {
+ connection.setConnectTimeout(CLIENT_TIMEOUT_MILLIS);
+ connection.setReadTimeout(CLIENT_TIMEOUT_MILLIS);
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(true);
+ connection.setChunkedStreamingMode(chunkSize);
+ try (OutputStream output = connection.getOutputStream()) {
+ output.write(data);
+ }
+ Assert.assertEquals(200, connection.getResponseCode());
+ ByteArrayOutputStream response = new ByteArrayOutputStream();
+ try (InputStream input = connection.getInputStream()) {
+ byte[] buffer = new byte[4096];
+ int read;
+ while ((read = input.read(buffer)) != -1) {
+ response.write(buffer, 0, read);
+ }
+ }
+ return new String(response.toByteArray(), StandardCharsets.UTF_8);
+ } finally {
+ connection.disconnect();
+ }
+ }
+
/** POSTs with the given entity and returns the response body as a string. */
private String postForBody(URI uri, HttpEntity entity) throws Exception {
HttpPost req = new HttpPost(uri);
@@ -390,7 +654,9 @@ private static String repeat(char c, int n) {
/** Sends a raw HTTP request over a socket and returns the full response (until EOF). */
private static String sendRaw(URI uri, String rawRequest) throws Exception {
- try (Socket socket = new Socket(uri.getHost(), uri.getPort())) {
+ try (Socket socket = new Socket()) {
+ socket.connect(new InetSocketAddress(uri.getHost(), uri.getPort()), CLIENT_TIMEOUT_MILLIS);
+ socket.setSoTimeout(CLIENT_TIMEOUT_MILLIS);
socket.getOutputStream().write(rawRequest.getBytes(StandardCharsets.US_ASCII));
socket.getOutputStream().flush();
ByteArrayOutputStream out = new ByteArrayOutputStream();
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java
index 8b3463895f8..3a4aa89ac29 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java
@@ -2023,7 +2023,7 @@ public void testWeb3ClientVersion() {
}
/**
- * Verifies SizeLimitHandler integration with the real JsonRpcServlet + jsonrpc4j stack.
+ * Verifies request size limit integration with the real JsonRpcServlet + jsonrpc4j stack.
*
* Covers: normal request no regression, Content-Length oversized 413,
* and chunked oversized 413 during streaming body reads.