Description
Service.sendBatch() matches batch responses to requests by array position (index i), not by the JSON-RPC id field. This violates the JSON-RPC 2.0 specification, which explicitly states:
"The Response objects being returned from a batch call MAY be returned in any order within the Array."
When a node returns responses in a different order than the requests were sent, web3j silently produces wrong data no exception, no error, just incorrect values assigned to the wrong response types.
Affected Code
Service.java lines 89–96:
for (int i = 0; i < nodes.size(); i++) {
Request<?, ? extends Response<?>> request = batchRequest.getRequests().get(i); // paired by index
Response<?> response =
objectMapper.treeToValue(nodes.get(i), request.getResponseType());
responses.add(response);
}
The Response.id field is populated by Jackson deserialization, and each Request has a unique id from DefaultIdProvider.getNextId(), but neither is used for correlation.
Reproduction
Concrete scenario:
Client sends batch:
[
{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":0},
{"jsonrpc":"2.0","method":"eth_getBalance","params":["0xabc...","latest"],"id":1}
]
Node returns responses in reversed order (valid per spec):
[
{"jsonrpc":"2.0","id":1,"result":"0x16345785d8a0000"}, ← getBalance result
{"jsonrpc":"2.0","id":0,"result":"0x10a0bd1"} ← blockNumber result
]
web3j behavior:
response[0] → deserialized as EthBlockNumber with value 0x16345785d8a0000 (WRONG : this is the balance)
response[1] → deserialized as EthGetBalance with value 0x10a0bd1 (WRONG : this is the block number)
No exception thrown. Completely wrong data returned silently.
Unit test proving the bug (passes against current main):
@Test
void sendBatch_outOfOrderResponse_showsMismatchBug() throws Exception {
// Two requests: eth_blockNumber (index 0), eth_getBalance (index 1)
BatchRequest batchRequest = web3j.newBatch()
.add(web3j.ethBlockNumber())
.add(web3j.ethGetBalance("0xabc0000000000000000000000000000000000001",
DefaultBlockParameterName.LATEST));
long blockNumberRequestId = batchRequest.getRequests().get(0).getId();
long getBalanceRequestId = batchRequest.getRequests().get(1).getId();
// Mock: responses in REVERSED order (valid per JSON-RPC 2.0)
buildResponse(
"["
+ "{\"jsonrpc\":\"2.0\",\"id\":" + getBalanceRequestId
+ ",\"result\":\"0x16345785d8a0000\"},"
+ "{\"jsonrpc\":\"2.0\",\"id\":" + blockNumberRequestId
+ ",\"result\":\"0x10a0bd1\"}"
+ "]");
BatchResponse response = batchRequest.send();
EthBlockNumber blockNum = (EthBlockNumber) response.getResponses().get(0);
EthGetBalance balance = (EthGetBalance) response.getResponses().get(1);
// BUG: values are swapped, block number contains balance, balance contains block number
assertEquals(new BigInteger("100000000000000000"), blockNum.getBlockNumber()); // WRONG value
assertEquals(new BigInteger("17435601"), balance.getBalance()); // WRONG value
}
Expected Behavior
Each response should be matched to its request using the id field, as required by the JSON-RPC 2.0 specification. The output order in BatchResponse should match the order of original requests regardless of response array order from the node.
Actual Behavior
Responses are matched positionally by array index. Out-of-order responses produce silently wrong data with no indication of error.
Proposed Fix
Match responses to requests by id instead of array position:
// Build id → JsonNode map from response array
Map<Long, JsonNode> responseById = new HashMap<>(nodes.size());
for (JsonNode node : nodes) {
long id = node.path("id").asLong(-1);
responseById.put(id, node);
}
// Match each request to its response by id, preserving request order
List<Response<?>> responses = new ArrayList<>(batchRequest.getRequests().size());
for (Request<?, ? extends Response<?>> req : batchRequest.getRequests()) {
JsonNode node = responseById.get(req.getId());
if (node == null) {
throw new IOException("Batch response missing entry for request id " + req.getId());
}
Response<?> response = objectMapper.treeToValue(node, req.getResponseType());
if (rawResponse != null) response.setRawResponse(rawResponse);
responses.add(response);
}
This fix is backward-compatible, in-order responses will still work correctly. The Response.id field is already populated by Jackson, and Request.id is already set by DefaultIdProvider.
Impact
Batch RPC is used by block scanners, event indexers, DEX aggregators, and multi-call patterns. Silent data corruption means wrong balances, wrong block numbers, or wrong receipt data,all without any exception to alert the developer. Nodes like go-ethereum process batch requests concurrently, making out-of-order responses possible in practice.
Environment
- web3j version: 5.0.x / main branch
- Affected files:
Service.java, potentially WebSocketService.java
Description
Service.sendBatch()matches batch responses to requests by array position (indexi), not by the JSON-RPCidfield. This violates the JSON-RPC 2.0 specification, which explicitly states:When a node returns responses in a different order than the requests were sent, web3j silently produces wrong data no exception, no error, just incorrect values assigned to the wrong response types.
Affected Code
Service.javalines 89–96:The
Response.idfield is populated by Jackson deserialization, and eachRequesthas a uniqueidfromDefaultIdProvider.getNextId(), but neither is used for correlation.Reproduction
Concrete scenario:
Unit test proving the bug (passes against current
main):Expected Behavior
Each response should be matched to its request using the
idfield, as required by the JSON-RPC 2.0 specification. The output order inBatchResponseshould match the order of original requests regardless of response array order from the node.Actual Behavior
Responses are matched positionally by array index. Out-of-order responses produce silently wrong data with no indication of error.
Proposed Fix
Match responses to requests by
idinstead of array position:This fix is backward-compatible, in-order responses will still work correctly. The
Response.idfield is already populated by Jackson, andRequest.idis already set byDefaultIdProvider.Impact
Batch RPC is used by block scanners, event indexers, DEX aggregators, and multi-call patterns. Silent data corruption means wrong balances, wrong block numbers, or wrong receipt data,all without any exception to alert the developer. Nodes like go-ethereum process batch requests concurrently, making out-of-order responses possible in practice.
Environment
Service.java, potentiallyWebSocketService.java