JSON-RPC Batch Requests

Batch requests allow multiple JSON-RPC calls to be sent in a single HTTP request, improving efficiency and reducing network overhead.

What You'll Learn

JavaScriptNode.jsIntermediate

Batch requests allow you to send multiple JSON-RPC calls in a single HTTP request, significantly reducing network overhead and improving performance. This example demonstrates how to implement and handle batch requests on both client and server sides.

Understanding Batch Requests

In JSON-RPC 2.0, batch requests allow clients to send multiple method calls in a single HTTP request. This is done by sending an array of individual JSON-RPC request objects instead of a single object. The server responds with an array of response objects in the same order as the requests.

Key benefits of batch requests include:

  • Reduced network overhead
  • Fewer HTTP connections
  • Parallel processing on the server
  • Improved overall performance

Server Implementation

Let's extend our basic server to handle batch requests. The server needs to detect when an array of requests is received and process each one individually, returning an array of responses.

batch-server.js
const express = require('express');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

// JSON-RPC method handlers
const methods = {
  add: (params) => {
    if (!params.a || !params.b) {
      throw { code: -32602, message: 'Invalid params', data: 'Parameters a and b are required' };
    }
    return params.a + params.b;
  },
  
  subtract: (params) => {
    if (!params.a || !params.b) {
      throw { code: -32602, message: 'Invalid params', data: 'Parameters a and b are required' };
    }
    return params.a - params.b;
  },
  
  multiply: (params) => {
    if (!params.a || !params.b) {
      throw { code: -32602, message: 'Invalid params', data: 'Parameters a and b are required' };
    }
    return params.a * params.b;
  },
  
  divide: (params) => {
    if (!params.a || !params.b) {
      throw { code: -32602, message: 'Invalid params', data: 'Parameters a and b are required' };
    }
    if (params.b === 0) {
      throw { code: 100, message: 'Division by zero', data: 'Cannot divide by zero' };
    }
    return params.a / params.b;
  },
  
  getSystemTime: () => {
    return new Date().toISOString();
  },
  
  echo: (params) => {
    return params;
  }
};

// Process a single JSON-RPC request
function processSingleRequest(request) {
  // Check for valid JSON-RPC 2.0 request
  if (!request.jsonrpc || request.jsonrpc !== '2.0') {
    return {
      jsonrpc: '2.0',
      error: { code: -32600, message: 'Invalid Request' },
      id: request.id || null
    };
  }
  
  // Check if method exists
  if (!methods[request.method]) {
    return {
      jsonrpc: '2.0',
      error: { code: -32601, message: 'Method not found' },
      id: request.id
    };
  }
  
  try {
    // Execute the method
    const result = methods[request.method](request.params || {});
    
    // Return successful response
    return {
      jsonrpc: '2.0',
      result: result,
      id: request.id
    };
  } catch (error) {
    // Handle method execution error
    return {
      jsonrpc: '2.0',
      error: {
        code: error.code || -32603,
        message: error.message || 'Internal error',
        data: error.data
      },
      id: request.id
    };
  }
}

// JSON-RPC request handler
app.post('/rpc', (req, res) => {
  const request = req.body;
  
  // Handle batch requests (array of requests)
  if (Array.isArray(request)) {
    // Check if the batch is empty
    if (request.length === 0) {
      return res.json({
        jsonrpc: '2.0',
        error: { code: -32600, message: 'Invalid Request - empty batch' },
        id: null
      });
    }
    
    // Process each request in the batch
    const responses = request.map(singleRequest => {
      return processSingleRequest(singleRequest);
    });
    
    // Return array of responses
    return res.json(responses);
  }
  
  // Handle single request
  const response = processSingleRequest(request);
  res.json(response);
});

// Start the server
const PORT = 3000;
app.listen(PORT, () => {
  console.log('JSON-RPC server with batch support running at http://localhost:'+PORT+'/rpc');
});

Client Implementation

Now, let's create a client that can send batch requests to our server.

batch-client.js
const fetch = require('node-fetch');

// JSON-RPC client class
class JsonRpcClient {
  constructor(serverUrl) {
    this.serverUrl = serverUrl;
    this.requestId = 1;
  }
  
  // Create a single request object
  createRequest(method, params) {
    return {
      jsonrpc: '2.0',
      method: method,
      params: params,
      id: this.requestId++
    };
  }
  
  // Send a single JSON-RPC request
  async call(method, params) {
    const request = this.createRequest(method, params);
    
    try {
      const response = await fetch(this.serverUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(request)
      });
      
      const jsonResponse = await response.json();
      
      if (jsonResponse.error) {
        throw new Error('Error ' + jsonResponse.error.code + ': ' + jsonResponse.error.message);
      }
      
      return jsonResponse.result;
    } catch (error) {
      console.error('JSON-RPC call failed:', error.message);
      throw error;
    }
  }
  
  // Send a batch of JSON-RPC requests
  async batchCall(requests) {
    // Convert method/params pairs to proper JSON-RPC request objects
    const batchRequests = requests.map(req => {
      return this.createRequest(req.method, req.params);
    });
    
    try {
      const response = await fetch(this.serverUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(batchRequests)
      });
      
      const jsonResponses = await response.json();
      
      // Process each response in the batch
      return jsonResponses.map(resp => {
        if (resp.error) {
          console.error('Error ' + resp.error.code + ': ' + resp.error.message);
          return null;
        }
        return resp.result;
      });
    } catch (error) {
      console.error('JSON-RPC batch call failed:', error.message);
      throw error;
    }
  }
}

// Example usage
async function main() {
  const client = new JsonRpcClient('http://localhost:3000/rpc');
  
  console.log("1. Single request example:");
  try {
    const sum = await client.call('add', { a: 5, b: 3 });
    console.log('5 + 3 =', sum);
  } catch (error) {
    console.error(error);
  }
  
  console.log("\n2. Batch request example:");
  try {
    const batchResults = await client.batchCall([
      { method: 'add', params: { a: 10, b: 20 } },
      { method: 'subtract', params: { a: 30, b: 5 } },
      { method: 'multiply', params: { a: 4, b: 7 } },
      { method: 'getSystemTime', params: {} },
      { method: 'echo', params: { message: 'Hello, batch requests!' } }
    ]);
    
    console.log('Batch results:');
    batchResults.forEach((result, index) => {
      console.log('Request #' + (index + 1) + ' result:', result);
    });
  } catch (error) {
    console.error(error);
  }
  
  console.log("\n3. Batch with errors example:");
  try {
    const mixedResults = await client.batchCall([
      { method: 'add', params: { a: 10, b: 20 } },
      { method: 'nonExistentMethod', params: {} },
      { method: 'divide', params: { a: 10, b: 0 } },
      { method: 'multiply', params: { a: 4, b: 7 } }
    ]);
    
    console.log('Mixed results:');
    mixedResults.forEach((result, index) => {
      console.log('Request #' + (index + 1) + ' result:', result);
    });
  } catch (error) {
    console.error(error);
  }
}

main();

How to Run the Example

  1. Ensure Node.js is installed on your system
  2. Create a new directory for the project
  3. Install dependencies: npm install express body-parser node-fetch
  4. Create the batch-server.js and batch-client.js files with the code above
  5. Start the server: node batch-server.js
  6. In a separate terminal, run the client: node batch-client.js

Expected Output

When you run the client script, you should see output similar to the following:

1. Single request example:
5 + 3 = 8

2. Batch request example:
Batch results:
Request #1 result: 30
Request #2 result: 25
Request #3 result: 28
Request #4 result: 2023-11-07T12:34:56.789Z
Request #5 result: { message: 'Hello, batch requests!' }

3. Batch with errors example:
Mixed results:
Request #1 result: 30
Request #2 result: null
Request #3 result: null
Request #4 result: 28

Key Concepts

Batch Request Format

A batch request is simply an array of individual JSON-RPC request objects:

[
  {
    "jsonrpc": "2.0",
    "method": "add",
    "params": { "a": 10, "b": 20 },
    "id": 1
  },
  {
    "jsonrpc": "2.0",
    "method": "subtract",
    "params": { "a": 30, "b": 5 },
    "id": 2
  }
]

Batch Response Format

The server responds with an array of response objects in the same order:

[
  {
    "jsonrpc": "2.0",
    "result": 30,
    "id": 1
  },
  {
    "jsonrpc": "2.0",
    "result": 25,
    "id": 2
  }
]

Error Handling in Batches

When an error occurs for a request in a batch, only that specific request fails. The other requests in the batch are still processed normally.

Performance Considerations

  • Batch Size: Keep batch sizes reasonable (typically 10-50 requests)
  • Request Independence: Batch requests that don't depend on each other
  • Error Recovery: Handle partial failures gracefully
  • Timeout Settings: Increase timeouts for batch requests as they may take longer

Best Practices

  • Related Operations: Group related operations in a single batch to ensure consistency
  • Request IDs: Use meaningful IDs to easily match responses to requests
  • Error Handling: Always check each response in a batch separately
  • Load Testing: Test with different batch sizes to find the optimal balance
  • Monitoring: Monitor server performance when processing batch requests

When to Use Batch Requests

Batch requests are particularly useful in the following scenarios:

  • Initial Data Loading: When a client needs to fetch multiple related datasets
  • Form Submission: When multiple validation or processing steps are needed
  • Dashboard Updates: When refreshing multiple independent widgets
  • High-Latency Networks: When reducing the number of round-trips is crucial