Skip to content

Commit 260585c

Browse files
authored
fix(jsonrpc): make the JSON-RPC output more compliant with specification (#6763)
1 parent 0dd5139 commit 260585c

2 files changed

Lines changed: 196 additions & 7 deletions

File tree

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

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.tron.core.services.jsonrpc;
22

3+
import com.fasterxml.jackson.core.JsonFactory;
34
import com.fasterxml.jackson.core.JsonProcessingException;
5+
import com.fasterxml.jackson.core.StreamReadConstraints;
46
import com.fasterxml.jackson.databind.JsonNode;
57
import com.fasterxml.jackson.databind.ObjectMapper;
68
import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -30,7 +32,19 @@
3032
@Slf4j(topic = "API")
3133
public class JsonRpcServlet extends RateLimiterServlet {
3234

33-
private static final ObjectMapper MAPPER = new ObjectMapper();
35+
// Snapshot of node.http.maxNestingDepth / maxTokenCount at class-load time (after Args.setParam).
36+
private static final ObjectMapper MAPPER = buildMapper();
37+
38+
private static ObjectMapper buildMapper() {
39+
CommonParameter p = CommonParameter.getInstance();
40+
JsonFactory factory = JsonFactory.builder()
41+
.streamReadConstraints(StreamReadConstraints.builder()
42+
.maxNestingDepth(p.getMaxNestingDepth())
43+
.maxTokenCount(p.getMaxTokenCount())
44+
.build())
45+
.build();
46+
return new ObjectMapper(factory);
47+
}
3448

3549
private enum JsonRpcError {
3650
PARSE_ERROR(-32700),
@@ -97,11 +111,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
97111
try {
98112
rootNode = MAPPER.readTree(body);
99113
if (rootNode == null || rootNode.isMissingNode()) {
100-
writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false);
114+
writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false);
101115
return;
102116
}
103117
} catch (JsonProcessingException e) {
104-
writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false);
118+
writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false);
119+
return;
120+
}
121+
122+
if (!rootNode.isObject() && !rootNode.isArray()) {
123+
writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", null, false);
105124
return;
106125
}
107126

@@ -159,15 +178,30 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes
159178
JsonNode subRequest = rootNode.get(i);
160179

161180
if (overflow) {
162-
// Notifications (no "id") do not get a response even on overflow.
163-
if (subRequest.has("id")) {
181+
if (!subRequest.isObject()) {
182+
batchResult.add(buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", null));
183+
} else if (subRequest.has("id")) {
184+
// Notifications (no "id") do not get a response even on overflow.
164185
batchResult.add(buildErrorNode(JsonRpcError.RESPONSE_TOO_LARGE,
165186
"Response exceeds the limit of " + maxResponseSize + " bytes",
166187
subRequest.get("id")));
167188
}
168189
continue;
169190
}
170191

192+
if (!subRequest.isObject()) {
193+
ObjectNode errNode = buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", null);
194+
byte[] errBytes = MAPPER.writeValueAsBytes(errNode);
195+
int addition = errBytes.length + (!batchResult.isEmpty() ? 1 : 0);
196+
if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) {
197+
overflow = true;
198+
} else {
199+
accumulatedSize += addition;
200+
}
201+
batchResult.add(errNode);
202+
continue;
203+
}
204+
171205
byte[] subBody;
172206
try {
173207
subBody = MAPPER.writeValueAsBytes(subRequest);
@@ -213,13 +247,14 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes
213247

214248
// JSON-RPC 2.0 §6: MUST NOT return an empty Array when there are no response objects.
215249
if (batchResult.isEmpty()) {
250+
resp.setContentType("application/json-rpc");
216251
resp.setStatus(HttpServletResponse.SC_OK);
217252
resp.setContentLength(0);
218253
return;
219254
}
220255

221256
byte[] finalBytes = MAPPER.writeValueAsBytes(batchResult);
222-
resp.setContentType("application/json-rpc; charset=utf-8");
257+
resp.setContentType("application/json-rpc");
223258
resp.setStatus(HttpServletResponse.SC_OK);
224259
resp.setContentLength(finalBytes.length);
225260
resp.getOutputStream().write(finalBytes);
@@ -261,7 +296,7 @@ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, Str
261296
} else {
262297
bytes = MAPPER.writeValueAsBytes(errorObj);
263298
}
264-
resp.setContentType("application/json-rpc; charset=utf-8");
299+
resp.setContentType("application/json-rpc");
265300
resp.setStatus(HttpServletResponse.SC_OK);
266301
resp.setContentLength(bytes.length);
267302
resp.getOutputStream().write(bytes);

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

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,160 @@ public void normalRequest_commitsRpcServerResponse() throws Exception {
245245
assertArrayEquals(rpcResp, resp.getContentAsByteArray());
246246
}
247247

248+
// --- Content-Type header: must be application/json-rpc (no charset suffix) ---
249+
250+
@Test
251+
public void errorResponse_contentTypeIsApplicationJsonRpc() throws Exception {
252+
MockHttpServletResponse resp = doPost("not valid json");
253+
assertEquals("application/json-rpc", resp.getContentType());
254+
}
255+
256+
@Test
257+
public void batchResponse_contentTypeIsApplicationJsonRpc() throws Exception {
258+
byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}"
259+
.getBytes(StandardCharsets.UTF_8);
260+
doAnswer(inv -> {
261+
OutputStream out = inv.getArgument(1);
262+
out.write(singleResp);
263+
return 0;
264+
}).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class));
265+
266+
MockHttpServletResponse resp = doPost("[{\"id\":1}]");
267+
assertEquals("application/json-rpc", resp.getContentType());
268+
}
269+
270+
@Test
271+
public void allNotificationBatch_contentTypeIsApplicationJsonRpc() throws Exception {
272+
// notification: rpcServer returns 0 bytes → empty batchResult → early return path
273+
doAnswer(inv -> 0).when(mockRpcServer)
274+
.handleRequest(any(InputStream.class), any(OutputStream.class));
275+
276+
MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]");
277+
assertEquals(200, resp.getStatus());
278+
assertEquals(0, resp.getContentLength());
279+
assertEquals("application/json-rpc", resp.getContentType());
280+
}
281+
282+
// --- Primitive root node → Invalid Request (-32600), id must be JSON null ---
283+
284+
@Test
285+
public void primitiveRootNull_returnsInvalidRequestWithJsonNullId() throws Exception {
286+
MockHttpServletResponse resp = doPost("null");
287+
assertEquals(200, resp.getStatus());
288+
JsonNode body = MAPPER.readTree(resp.getContentAsString());
289+
assertFalse(body.isArray());
290+
assertEquals("2.0", body.get("jsonrpc").asText());
291+
assertEquals(-32600, body.get("error").get("code").asInt());
292+
assertTrue("id must be JSON null, not the string \"null\"", body.get("id").isNull());
293+
assertFalse("id must not be a string", body.get("id").isTextual());
294+
}
295+
296+
@Test
297+
public void primitiveRootBoolean_returnsInvalidRequest() throws Exception {
298+
MockHttpServletResponse resp = doPost("true");
299+
assertEquals(200, resp.getStatus());
300+
assertEquals(-32600,
301+
MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt());
302+
}
303+
304+
@Test
305+
public void primitiveRootNumber_returnsInvalidRequest() throws Exception {
306+
MockHttpServletResponse resp = doPost("123");
307+
assertEquals(200, resp.getStatus());
308+
assertEquals(-32600,
309+
MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt());
310+
}
311+
312+
@Test
313+
public void primitiveRootString_returnsInvalidRequest() throws Exception {
314+
MockHttpServletResponse resp = doPost("\"hello\"");
315+
assertEquals(200, resp.getStatus());
316+
assertEquals(-32600,
317+
MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt());
318+
}
319+
320+
// --- Non-object element inside a batch → Invalid Request per element ---
321+
322+
@Test
323+
public void batchWithNestedArray_returnsInvalidRequestArray() throws Exception {
324+
MockHttpServletResponse resp = doPost("[[]]");
325+
assertEquals(200, resp.getStatus());
326+
JsonNode body = MAPPER.readTree(resp.getContentAsString());
327+
assertTrue("response must be a JSON array", body.isArray());
328+
assertEquals(1, body.size());
329+
assertEquals(-32600, body.get(0).get("error").get("code").asInt());
330+
assertTrue("id in batch error must be JSON null", body.get(0).get("id").isNull());
331+
}
332+
333+
@Test
334+
public void batchWithMixedObjectAndArray_objectProcessedArrayRejected() throws Exception {
335+
byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}"
336+
.getBytes(StandardCharsets.UTF_8);
337+
doAnswer(inv -> {
338+
OutputStream out = inv.getArgument(1);
339+
out.write(singleResp);
340+
return 0;
341+
}).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class));
342+
343+
MockHttpServletResponse resp = doPost("[{\"id\":1}, []]");
344+
assertEquals(200, resp.getStatus());
345+
JsonNode body = MAPPER.readTree(resp.getContentAsString());
346+
assertTrue("response must be a JSON array", body.isArray());
347+
assertEquals(2, body.size());
348+
assertEquals("ok", body.get(0).get("result").asText());
349+
assertEquals(-32600, body.get(1).get("error").get("code").asInt());
350+
}
351+
352+
@Test
353+
public void batchWithNumericAndStringElements_allGetInvalidRequest() throws Exception {
354+
MockHttpServletResponse resp = doPost("[42, \"foo\", true]");
355+
assertEquals(200, resp.getStatus());
356+
JsonNode body = MAPPER.readTree(resp.getContentAsString());
357+
assertTrue("response must be a JSON array", body.isArray());
358+
assertEquals(3, body.size());
359+
for (int i = 0; i < 3; i++) {
360+
assertEquals(-32600, body.get(i).get("error").get("code").asInt());
361+
}
362+
}
363+
364+
// --- StreamReadConstraints: maxNestingDepth and maxTokenCount must be enforced ---
365+
366+
@Test
367+
public void excessivelyNestedRequest_returnsParseError() throws Exception {
368+
int limit = CommonParameter.getInstance().getMaxNestingDepth();
369+
StringBuilder sb = new StringBuilder();
370+
for (int i = 0; i <= limit; i++) {
371+
sb.append('[');
372+
}
373+
sb.append('0');
374+
for (int i = 0; i <= limit; i++) {
375+
sb.append(']');
376+
}
377+
378+
MockHttpServletResponse resp = doPost(sb.toString());
379+
assertEquals(200, resp.getStatus());
380+
assertEquals(-32700,
381+
MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt());
382+
}
383+
384+
@Test
385+
public void tooManyTokens_returnsParseError() throws Exception {
386+
int limit = CommonParameter.getInstance().getMaxTokenCount();
387+
StringBuilder sb = new StringBuilder("[");
388+
for (int i = 0; i < limit; i++) {
389+
if (i > 0) {
390+
sb.append(',');
391+
}
392+
sb.append('0');
393+
}
394+
sb.append(']');
395+
396+
MockHttpServletResponse resp = doPost(sb.toString());
397+
assertEquals(200, resp.getStatus());
398+
assertEquals(-32700,
399+
MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt());
400+
}
401+
248402
// --- helpers ---
249403

250404
private MockHttpServletResponse doPost(String body) throws Exception {

0 commit comments

Comments
 (0)