diff --git a/framework/src/main/java/org/tron/core/services/filter/HttpInterceptor.java b/framework/src/main/java/org/tron/core/services/filter/HttpInterceptor.java index 2ff8a5ad32..ed20630b78 100644 --- a/framework/src/main/java/org/tron/core/services/filter/HttpInterceptor.java +++ b/framework/src/main/java/org/tron/core/services/filter/HttpInterceptor.java @@ -1,6 +1,5 @@ package org.tron.core.services.filter; -import com.google.common.base.Strings; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -9,6 +8,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpStatus; import org.tron.common.prometheus.MetricKeys; import org.tron.common.prometheus.MetricLabels; import org.tron.common.prometheus.Metrics; @@ -66,6 +67,10 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } MetricsUtil.meterMark(MetricsKey.NET_API_QPS, 1); MetricsUtil.meterMark(MetricsKey.NET_API_FAIL_QPS, 1); + if (e instanceof BadMessageException + && ((BadMessageException) e).getCode() == HttpStatus.PAYLOAD_TOO_LARGE_413) { + throw (BadMessageException) e; + } } } @@ -75,4 +80,3 @@ public void destroy() { } - diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index f488c32df4..b5ae7d5862 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -14,6 +14,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.BadMessageException; import org.springframework.beans.factory.annotation.Autowired; import org.tron.common.parameter.RateLimiterInitialization; import org.tron.common.prometheus.MetricKeys; @@ -133,7 +134,7 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) resp.getWriter() .println(Util.printErrorMsg(new IllegalAccessException("lack of computing resources"))); } - } catch (ServletException | IOException e) { + } catch (ServletException | IOException | BadMessageException e) { throw e; } catch (Exception unexpected) { logger.error("Http Api {}, Method:{}. Error:", url, req.getMethod(), unexpected); @@ -149,4 +150,4 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) } } } -} \ No newline at end of file +} 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 75bb59d2e5..e8d14ace06 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -26,6 +26,7 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.bouncycastle.util.encoders.Hex; +import org.eclipse.jetty.http.HttpStatus; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -1572,8 +1573,7 @@ public void testWeb3ClientVersion() { * Verifies SizeLimitHandler integration with the real JsonRpcServlet + jsonrpc4j stack. * * Covers: normal request no regression, Content-Length oversized 413, - * and chunked oversized handled gracefully (body truncated, 200 + empty body - * because jsonrpc4j absorbs the BadMessageException). + * and chunked oversized 413 during streaming body reads. */ @Test public void testJsonRpcSizeLimitIntegration() { @@ -1609,11 +1609,11 @@ public void testJsonRpcSizeLimitIntegration() { overPost.setEntity(new StringEntity( new String(new char[(int) testLimit + 1]).replace('\0', 'x'))); resp = httpClient.execute(overPost); - Assert.assertEquals(413, resp.getStatusLine().getStatusCode()); + Assert.assertEquals(HttpStatus.PAYLOAD_TOO_LARGE_413, + resp.getStatusLine().getStatusCode()); resp.close(); - // Chunked oversized -> BadMessageException thrown during body read, - // absorbed by jsonrpc4j catch(Exception) -> 200 with empty body. + // Chunked oversized -> BadMessageException thrown during body read. // Body read IS truncated at the limit - OOM protection effective. byte[] chunkedData = new String(new char[(int) testLimit * 2]) .replace('\0', 'x').getBytes("UTF-8"); @@ -1621,10 +1621,8 @@ public void testJsonRpcSizeLimitIntegration() { chunkedPost.setEntity(new InputStreamEntity( new ByteArrayInputStream(chunkedData), -1)); resp = httpClient.execute(chunkedPost); - Assert.assertEquals(200, resp.getStatusLine().getStatusCode()); - body = EntityUtils.toString(resp.getEntity()); - Assert.assertTrue("Chunked oversized should return empty body" - + " (jsonrpc4j absorbs BadMessageException)", body.isEmpty()); + Assert.assertEquals(HttpStatus.PAYLOAD_TOO_LARGE_413, + resp.getStatusLine().getStatusCode()); resp.close(); } } catch (Exception e) { diff --git a/framework/src/test/java/org/tron/core/services/filter/HttpInterceptorTest.java b/framework/src/test/java/org/tron/core/services/filter/HttpInterceptorTest.java new file mode 100644 index 0000000000..b293e8047b --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/filter/HttpInterceptorTest.java @@ -0,0 +1,44 @@ +package org.tron.core.services.filter; + +import static org.junit.Assert.assertThrows; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +public class HttpInterceptorTest { + + private final HttpInterceptor interceptor = new HttpInterceptor(); + + @Test + public void testOversizedBadMessagePropagates() { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/jsonrpc"); + request.setServletPath("/jsonrpc"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain chain = (req, resp) -> { + throw new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413, + "Request body is too large"); + }; + + BadMessageException e = assertThrows(BadMessageException.class, + () -> interceptor.doFilter(request, response, chain)); + + org.junit.Assert.assertEquals(HttpStatus.PAYLOAD_TOO_LARGE_413, e.getCode()); + } + + @Test + public void testNonOversizedExceptionIsStillSwallowed() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/jsonrpc"); + request.setServletPath("/jsonrpc"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain chain = (req, resp) -> { + throw new ServletException("expected"); + }; + + interceptor.doFilter(request, response, chain); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java index 8cca558d15..26826c5709 100644 --- a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java @@ -16,6 +16,8 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpStatus; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; @@ -69,6 +71,14 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) { } } + static class OversizedRequestServlet extends RateLimiterServlet { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + throw new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413, + "Request body is too large"); + } + } + /** * GlobalRateLimiter's static initializer calls Args.getInstance().getRateLimiterGlobalQps(). * Without Args being initialized the default QPS is 0, causing RateLimiter.create(0) to throw. @@ -250,4 +260,24 @@ public void testNullRateLimiterConsultsOnlyGlobal() throws Exception { globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), times(1)); } } + + @Test + public void testOversizedRequestBadMessagePropagates() throws Exception { + OversizedRequestServlet oversizedServlet = new OversizedRequestServlet(); + Field f = RateLimiterServlet.class.getDeclaredField("container"); + f.setAccessible(true); + f.set(oversizedServlet, container); + MockHttpServletRequest postRequest = new MockHttpServletRequest("POST", "/jsonrpc"); + postRequest.setRemoteAddr("10.0.0.1"); + postRequest.setServletPath("/jsonrpc"); + + try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); + + BadMessageException e = assertThrows(BadMessageException.class, + () -> oversizedServlet.service(postRequest, response)); + + assertEquals(HttpStatus.PAYLOAD_TOO_LARGE_413, e.getCode()); + } + } }