JSON-RPC Notifications Example

This example demonstrates how to implement and use JSON-RPC 2.0 notifications - a special type of request that doesn't require a response from the server, enabling fire-and-forget communication patterns.

What You'll Learn

  • What JSON-RPC notifications are and when to use them
  • How to send notifications from a client
  • How to handle notifications on the server
  • Best practices for implementing notification-based systems
  • Performance benefits of using notifications

1. What are JSON-RPC Notifications?

In the JSON-RPC 2.0 specification, a notification is a special type of request that does not require a response from the server. Notifications are identified by the absence of an id field in the request.

Key characteristics of notifications:

  • No id field is present in the request
  • The server must not reply to a notification
  • Client cannot know if the notification was processed successfully
  • Useful for "fire-and-forget" scenarios where no acknowledgment is needed

When to Use Notifications

Notifications are ideal for:

  • Logging events without waiting for confirmation
  • Sending analytics data to the server
  • Broadcasting status updates
  • Any scenario where you don't need to confirm receipt or handle errors
Example: Regular Request vs. Notification
// Regular JSON-RPC request{
  "jsonrpc": "2.0",
  "method": "updateUser",
  "params": { "userId": 123, "name": "John Doe" },
  "id": 1
}// JSON-RPC notification (no id field){
  "jsonrpc": "2.0",
  "method": "logEvent",
  "params": { "event": "user_login", "userId": 123 }
}

2. Server Implementation

Let's create a server that can handle both regular JSON-RPC requests and notifications. This server will have special logic to recognize and process notifications without generating responses.

server.jsExpress.js Server with Notification Support
const express = require('express');
const bodyParser = require('body-parser');

const app = express();
const PORT = 3000;

// Middleware to parse JSON request body
app.use(bodyParser.json());

// Simple in-memory log for demonstration
const eventLog = [];

// JSON-RPC request handler
app.post('/jsonrpc', (req, res) => {
  const request = req.body;
  
  // Validate JSON-RPC request
  if (!request.jsonrpc || request.jsonrpc !== '2.0' || !request.method) {
    return res.json({
      jsonrpc: '2.0',
      error: {
        code: -32600,
        message: 'Invalid Request'
      },
      id: request.id || null
    });
  }

  // Check if it's a notification (no id field)
  const isNotification = request.id === undefined;

  // Process method calls
  let result;
  let error;

  try {
    switch (request.method) {
      // Regular methods that return a response
      case 'echo':
        result = request.params;
        break;
      
      case 'add':
        if (!Array.isArray(request.params) || request.params.length !== 2) {
          throw { code: -32602, message: 'Invalid params' };
        }
        result = request.params[0] + request.params[1];
        break;
      
      // Methods often used as notifications
      case 'logEvent':
        if (!request.params || !request.params.event) {
          throw { code: -32602, message: 'Invalid params, event is required' };
        }
        
        // Add timestamp to the event
        const logEntry = {
          ...request.params,
          timestamp: new Date().toISOString()
        };
        
        // Store in our log
        eventLog.push(logEntry);
        console.log('Event logged:', logEntry);
        
        result = true;
        break;
        
      case 'updateStatus':
        if (!request.params || !request.params.status) {
          throw { code: -32602, message: 'Invalid params, status is required' };
        }
        
        console.log('Status updated:', request.params.status);
        result = true;
        break;
        
      default:
        throw { code: -32601, message: 'Method not found' };
    }
  } catch (e) {
    error = {
      code: e.code || -32603,
      message: e.message || 'Internal error'
    };
  }

  // For notifications, don't send a response
  if (isNotification) {
    // Log errors from notifications for debugging
    if (error) {
      console.error('Error processing notification:', error);
    }
    
    // End the request without a response body
    return res.status(204).end();
  }

  // For regular requests, build and send a response
  const response = {
    jsonrpc: '2.0',
    id: request.id
  };

  if (error) {
    response.error = error;
  } else {
    response.result = result;
  }

  res.json(response);
});

// Endpoint to view the event log (for demonstration purposes)
app.get('/log', (req, res) => {
  res.json(eventLog);
});

// Start the server
app.listen(PORT, () => {
  console.log(`JSON-RPC server running at http://localhost:${PORT}`);
});

While we return HTTP 204 (No Content) for notifications in this example, the JSON-RPC spec only requires that no response be sent. In a real-world implementation, you might want to batch process notifications asynchronously for better performance.

Key Server Implementation Details:

  1. Notification Detection: We check for the absence of an id field to identify notifications.
  2. Response Handling: For notifications, we return HTTP 204 (No Content) rather than a JSON-RPC response.
  3. Error Logging: We still log errors from notifications server-side, even though we don't return them to the client.
  4. Dual-Purpose Methods: Methods like logEvent can be called both as regular requests or as notifications.

3. Client Implementation

Now let's create a client that can send both regular requests and notifications to our server:

client.jsJSON-RPC Client with Notification Support
const fetch = require('node-fetch');

class JsonRpcClient {
  constructor(url) {
    this.url = url;
    this.id = 1;
  }

  // Method for regular JSON-RPC requests (with response)
  async call(method, params) {
    const request = {
      jsonrpc: '2.0',
      method,
      params,
      id: this.id++
    };

    const response = await fetch(this.url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(request)
    });

    // Parse the JSON response
    const result = await response.json();

    if (result.error) {
      throw new Error(`Error ${result.error.code}: ${result.error.message}`);
    }

    return result.result;
  }

  // Method for sending notifications (no response expected)
  async notify(method, params) {
    const notification = {
      jsonrpc: '2.0',
      method,
      params
      // No id field for notifications
    };

    const response = await fetch(this.url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(notification)
    });

    // We don't expect a response body, but we should check the status
    if (response.status !== 204) {
      console.warn(`Unexpected status code ${response.status} for notification`);
    }

    // No return value for notifications
    return;
  }
}

// Create client instance
const client = new JsonRpcClient('http://localhost:3000/jsonrpc');

// Example usage
async function runExamples() {
  try {
    // Regular request - we wait for a response
    console.log('Sending a regular request...');
    const result = await client.call('add', [5, 3]);
    console.log('Result of 5 + 3:', result);

    // Send some notifications - we don't wait for responses
    console.log('\nSending notifications...');
    
    // Log a user login event
    await client.notify('logEvent', {
      event: 'user_login',
      userId: 123,
      details: { ip: '192.168.1.1', browser: 'Chrome' }
    });
    console.log('Login event notification sent');

    // Update application status
    await client.notify('updateStatus', {
      status: 'active',
      component: 'backend',
      load: 0.75
    });
    console.log('Status update notification sent');

    // Send another regular request to view the log
    console.log('\nChecking server log...');
    const logUrl = 'http://localhost:3000/log';
    const logResponse = await fetch(logUrl);
    const logData = await logResponse.json();
    console.log('Server event log:', JSON.stringify(logData, null, 2));
    
  } catch (e) {
    console.error('Error:', e);
  }
}

// Run the examples
runExamples();

Key Client Implementation Details:

  1. Dual Methods: We implement both call() for regular requests and notify() for notifications.
  2. No ID for Notifications: We intentionally omit the id field when sending notifications.
  3. Status Check: For notifications, we check that the server returns HTTP 204 (No Content) but don't expect any response body.
  4. No Return Value: The notify() method doesn't return a result since we don't get one from the server.

4. Running the Example

To run this example:

  1. Save both files (server.js and client.js) in the same directory
  2. Install the required packages with npm install express body-parser node-fetch
  3. Start the server in one terminal with node server.js
  4. Run the client in another terminal with node client.js

You should see output similar to this:

Sending a regular request...
Result of 5 + 3: 8

Sending notifications...
Login event notification sent
Status update notification sent

Checking server log...
Server event log: [
  {
    "event": "user_login",
    "userId": 123,
    "details": {
      "ip": "192.168.1.1",
      "browser": "Chrome"
    },
    "timestamp": "2023-08-15T14:52:30.123Z"
  }
]

At the same time, in the server console, you'll see:

JSON-RPC server running at http://localhost:3000
Event logged: {
  event: 'user_login',
  userId: 123,
  details: { ip: '192.168.1.1', browser: 'Chrome' },
  timestamp: '2023-08-15T14:52:30.123Z'
}
Status updated: active

5. Best Practices for Notifications

When to Use Notifications

  • For events that don't need acknowledgment
  • For logging and analytics purposes
  • For status updates that aren't critical
  • When you want to optimize network performance

When NOT to Use Notifications

  • For operations where success confirmation is needed
  • When error handling is important
  • For critical operations (e.g., financial transactions)
  • When you need the result of the operation

Performance Considerations

  • Notifications can reduce network overhead since no response is returned
  • Consider batching multiple notifications together for even better performance
  • Process notifications asynchronously on the server when possible
  • Use a queue system for high-volume notifications to prevent server overload

Error Handling Strategy

Since clients cannot receive error responses for notifications, consider these strategies:

  • Log errors server-side for debugging
  • Implement a periodic health check request (not a notification) to verify the system is functioning
  • For critical operations, use regular requests instead of notifications
  • Consider implementing a separate feedback channel (e.g., WebSockets) for important systems