Skip to content

Service.sendBatch() pairs responses by array index instead of JSON-RPC id, causing silent data corruption with out-of-order responses #2316

Description

@anandanmol1010

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions