Skip to content

Commit 998f52f

Browse files
committed
don't continue rest jsonrpc request of batch if overflow
1 parent c329b47 commit 998f52f

2 files changed

Lines changed: 119 additions & 24 deletions

File tree

framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.googlecode.jsonrpc4j.JsonRpcInterceptor;
1010
import com.googlecode.jsonrpc4j.JsonRpcServer;
1111
import com.googlecode.jsonrpc4j.ProxyUtil;
12+
import java.io.ByteArrayInputStream;
1213
import java.io.ByteArrayOutputStream;
1314
import java.io.IOException;
1415
import java.io.InputStream;
@@ -116,29 +117,94 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
116117
return;
117118
}
118119

120+
int maxResponseSize = parameter.getJsonRpcMaxResponseSize();
121+
if (isBatch) {
122+
handleBatch(resp, rootNode, maxResponseSize);
123+
} else {
124+
handleSingle(req, resp, rootNode, body, maxResponseSize);
125+
}
126+
}
127+
128+
private void handleSingle(HttpServletRequest req, HttpServletResponse resp,
129+
JsonNode rootNode, byte[] body, int maxResponseSize) throws IOException {
119130
CachedBodyRequestWrapper cachedReq = new CachedBodyRequestWrapper(req, body);
120131
BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(
121-
resp, parameter.getJsonRpcMaxResponseSize());
132+
resp, maxResponseSize);
122133

123134
try {
124135
rpcServer.handle(cachedReq, bufferedResp);
125136
} catch (RuntimeException e) {
126137
logger.error("RPC execution failed", e);
127-
JsonNode idNode = isBatch ? null : rootNode.get("id");
128-
writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", idNode, isBatch);
138+
writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error",
139+
rootNode.get("id"), false);
129140
return;
130141
}
131142

132143
if (bufferedResp.isOverflow()) {
133-
JsonNode idNode = isBatch ? null : rootNode.get("id");
134144
writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE,
135-
"Response exceeds the limit of " + parameter.getJsonRpcMaxResponseSize() + " bytes",
136-
idNode, isBatch);
145+
"Response exceeds the limit of " + maxResponseSize + " bytes",
146+
rootNode.get("id"), false);
137147
return;
138148
}
139149
bufferedResp.commitToResponse();
140150
}
141151

152+
private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxResponseSize)
153+
throws IOException {
154+
155+
ArrayNode batchResult = MAPPER.createArrayNode();
156+
int accumulatedSize = 2; // "[]"
157+
158+
for (int i = 0; i < rootNode.size(); i++) {
159+
byte[] subBody;
160+
try {
161+
subBody = MAPPER.writeValueAsBytes(rootNode.get(i));
162+
} catch (JsonProcessingException e) {
163+
writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true);
164+
return;
165+
}
166+
167+
ByteArrayOutputStream subOutput = new ByteArrayOutputStream();
168+
try {
169+
rpcServer.handleRequest(new ByteArrayInputStream(subBody), subOutput);
170+
} catch (RuntimeException e) {
171+
logger.error("RPC execution failed for batch sub-request {}", i, e);
172+
writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true);
173+
return;
174+
}
175+
176+
byte[] responseBytes = subOutput.toByteArray();
177+
if (responseBytes.length == 0) {
178+
continue; // notification — no response
179+
}
180+
181+
// comma separator between array elements
182+
int addition = responseBytes.length + (!batchResult.isEmpty() ? 1 : 0);
183+
if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) {
184+
writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE,
185+
"Response exceeds the limit of " + maxResponseSize + " bytes", null, true);
186+
return;
187+
}
188+
accumulatedSize += addition;
189+
190+
JsonNode responseNode;
191+
try {
192+
responseNode = MAPPER.readTree(responseBytes);
193+
} catch (IOException e) {
194+
writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true);
195+
return;
196+
}
197+
batchResult.add(responseNode);
198+
}
199+
200+
byte[] finalBytes = MAPPER.writeValueAsBytes(batchResult);
201+
resp.setContentType("application/json; charset=utf-8");
202+
resp.setStatus(HttpServletResponse.SC_OK);
203+
resp.setContentLength(finalBytes.length);
204+
resp.getOutputStream().write(finalBytes);
205+
resp.getOutputStream().flush();
206+
}
207+
142208
private byte[] readBody(InputStream in) throws IOException {
143209
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
144210
byte[] tmp = new byte[4096];

framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import com.fasterxml.jackson.databind.ObjectMapper;
1414
import com.googlecode.jsonrpc4j.JsonRpcServer;
1515
import java.io.IOException;
16+
import java.io.InputStream;
17+
import java.io.OutputStream;
1618
import java.lang.reflect.Field;
1719
import java.nio.charset.StandardCharsets;
1820
import javax.servlet.http.HttpServletRequest;
@@ -87,16 +89,20 @@ public void batchExceedsLimit_returnsExceedLimitAsArray() throws Exception {
8789
@Test
8890
public void batchWithinLimit_proceedsToRpcServer() throws Exception {
8991
CommonParameter.getInstance().jsonRpcMaxBatchSize = 5;
90-
byte[] rpcResp = "[{\"result\":\"ok\"}]".getBytes(StandardCharsets.UTF_8);
92+
byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}"
93+
.getBytes(StandardCharsets.UTF_8);
9194
doAnswer(inv -> {
92-
HttpServletResponse r = inv.getArgument(1);
93-
r.getOutputStream().write(rpcResp);
94-
return null;
95-
}).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
95+
OutputStream out = inv.getArgument(1);
96+
out.write(singleResp);
97+
return 0;
98+
}).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class));
9699

97100
MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2}]");
98101
assertEquals(200, resp.getStatus());
99-
assertArrayEquals(rpcResp, resp.getContentAsByteArray());
102+
JsonNode body = MAPPER.readTree(resp.getContentAsByteArray());
103+
assertTrue("batch response must be a JSON array", body.isArray());
104+
assertEquals("each sub-request must produce a response", 2, body.size());
105+
assertEquals("ok", body.get(0).get("result").asText());
100106
}
101107

102108
@Test
@@ -113,12 +119,9 @@ public void emptyBatch_returnsInvalidRequest() throws Exception {
113119
@Test
114120
public void batchLimitDisabled_largeBatchAllowed() throws Exception {
115121
CommonParameter.getInstance().jsonRpcMaxBatchSize = 0;
116-
byte[] rpcResp = "[]".getBytes(StandardCharsets.UTF_8);
117-
doAnswer(inv -> {
118-
HttpServletResponse r = inv.getArgument(1);
119-
r.getOutputStream().write(rpcResp);
120-
return null;
121-
}).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
122+
// write nothing — simulates notifications (no response expected)
123+
doAnswer(inv -> 0).when(mockRpcServer)
124+
.handleRequest(any(InputStream.class), any(OutputStream.class));
122125

123126
StringBuilder sb = new StringBuilder("[");
124127
for (int i = 0; i < 500; i++) {
@@ -130,7 +133,9 @@ public void batchLimitDisabled_largeBatchAllowed() throws Exception {
130133
sb.append("]");
131134
MockHttpServletResponse resp = doPost(sb.toString());
132135
assertEquals(200, resp.getStatus());
133-
assertArrayEquals(rpcResp, resp.getContentAsByteArray());
136+
JsonNode body = MAPPER.readTree(resp.getContentAsByteArray());
137+
assertTrue("response must be a JSON array", body.isArray());
138+
assertEquals("all notifications produce no response entries", 0, body.size());
134139
}
135140

136141
// --- rpcServer.handle exceptions ---
@@ -149,7 +154,7 @@ public void rpcServerThrowsRuntimeException_returnsInternalError() throws Except
149154
@Test
150155
public void batchRpcServerThrows_internalErrorIsArray() throws Exception {
151156
doThrow(new RuntimeException("boom")).when(mockRpcServer)
152-
.handle((HttpServletRequest) any(), any());
157+
.handleRequest(any(InputStream.class), any(OutputStream.class));
153158
MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]");
154159
assertEquals(200, resp.getStatus());
155160
JsonNode body = MAPPER.readTree(resp.getContentAsString());
@@ -181,10 +186,10 @@ public void batchResponseTooLarge_returnsErrorArray() throws Exception {
181186
int limit = 50;
182187
CommonParameter.getInstance().jsonRpcMaxResponseSize = limit;
183188
doAnswer(inv -> {
184-
HttpServletResponse r = inv.getArgument(1);
185-
r.getOutputStream().write(new byte[limit + 1]);
186-
return null;
187-
}).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
189+
OutputStream out = inv.getArgument(1);
190+
out.write(new byte[limit + 1]);
191+
return 0;
192+
}).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class));
188193

189194
MockHttpServletResponse resp = doPost("[{\"method\":\"eth_getLogs\"}]");
190195
assertEquals(200, resp.getStatus());
@@ -193,6 +198,30 @@ public void batchResponseTooLarge_returnsErrorArray() throws Exception {
193198
assertEquals(-32003, body.get(0).get("error").get("code").asInt());
194199
}
195200

201+
@Test
202+
public void batchShortCircuitsOnOverflow() throws Exception {
203+
int limit = 50;
204+
CommonParameter.getInstance().jsonRpcMaxResponseSize = limit;
205+
int[] callCount = {0};
206+
doAnswer(inv -> {
207+
OutputStream out = inv.getArgument(1);
208+
callCount[0]++;
209+
if (callCount[0] == 1) {
210+
out.write("{\"result\":\"ok\"}".getBytes(StandardCharsets.UTF_8));
211+
} else {
212+
out.write(new byte[limit]); // triggers overflow when added to accumulated size
213+
}
214+
return 0;
215+
}).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class));
216+
217+
MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2},{\"id\":3}]");
218+
assertEquals(200, resp.getStatus());
219+
JsonNode body = MAPPER.readTree(resp.getContentAsString());
220+
assertTrue("overflow response must be an array", body.isArray());
221+
assertEquals(-32003, body.get(0).get("error").get("code").asInt());
222+
assertEquals("third sub-request must not be executed after overflow", 2, callCount[0]);
223+
}
224+
196225
// --- normal path ---
197226

198227
@Test

0 commit comments

Comments
 (0)