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
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.
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.
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
- Ensure Node.js is installed on your system
- Create a new directory for the project
- Install dependencies:
npm install express body-parser node-fetch
- Create the batch-server.js and batch-client.js files with the code above
- Start the server:
node batch-server.js
- 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