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

JSON-RPC Notifications

Understand JSON-RPC notifications - one-way requests that don't expect a response.

What are Notifications?

A JSON-RPC notification is a request object without an "id" member. The client is not interested in the response and does not expect one. The server must not reply to a notification, including when the notification is invalid.

Regular Request

{
  "jsonrpc": "2.0",
  "method": "subtract",
  "params": [42, 23],
  "id": 1
}

Expects a response

Notification

{
  "jsonrpc": "2.0",
  "method": "subtract",
  "params": [42, 23]
}

No response expected

Common Use Cases

1. Logging and Analytics

Send analytics data or log events without waiting for confirmation.

{
  "jsonrpc": "2.0",
  "method": "log.event",
  "params": {
    "userId": "12345",
    "action": "page_view",
    "page": "/products/laptop",
    "timestamp": "2024-01-15T10:30:00Z",
    "metadata": {
      "userAgent": "Mozilla/5.0...",
      "referrer": "https://google.com"
    }
  }
}

2. Cache Invalidation

Notify cache servers to invalidate specific data without waiting for confirmation.

{
  "jsonrpc": "2.0",
  "method": "cache.invalidate",
  "params": {
    "keys": ["user:12345", "user:12345:profile"],
    "reason": "profile_updated"
  }
}

3. Background Tasks

Trigger background processing without blocking the client.

{
  "jsonrpc": "2.0",
  "method": "email.send",
  "params": {
    "to": "[email protected]",
    "subject": "Welcome to our service",
    "template": "welcome",
    "variables": {
      "name": "John Doe",
      "activationLink": "https://example.com/activate/abc123"
    }
  }
}

4. Real-time Updates

Send real-time updates to connected clients (often via WebSocket).

{
  "jsonrpc": "2.0",
  "method": "user.status.changed",
  "params": {
    "userId": "12345",
    "status": "online",
    "lastSeen": "2024-01-15T10:30:00Z"
  }
}

Batch Notifications

You can send multiple notifications in a single batch request. This is useful for optimizing network traffic.

Multiple Events Example

[
  {
    "jsonrpc": "2.0",
    "method": "user.login",
    "params": {
      "userId": "12345",
      "timestamp": "2024-01-15T10:30:00Z",
      "ip": "192.168.1.1"
    }
  },
  {
    "jsonrpc": "2.0",
    "method": "analytics.track",
    "params": {
      "event": "session_start",
      "userId": "12345",
      "properties": {
        "platform": "web",
        "browser": "Chrome"
      }
    }
  },
  {
    "jsonrpc": "2.0",
    "method": "cache.warm",
    "params": {
      "userId": "12345",
      "preload": ["preferences", "recent_items"]
    }
  }
]

Implementation Examples

JavaScript Client

class JsonRpcClient {
  constructor(endpoint) {
    this.endpoint = endpoint;
  }
  
  // Send notification (no response expected)
  async notify(method, params) {
    const notification = {
      jsonrpc: '2.0',
      method,
      params
    };
    
    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(notification)
      });
      // No response handling - fire and forget
    } catch (error) {
      // Only handle network errors
      console.error('Network error sending notification:', error);
    }
  }
  
  // Send multiple notifications in batch
  async notifyBatch(notifications) {
    const batch = notifications.map(({ method, params }) => ({
      jsonrpc: '2.0',
      method,
      params
    }));
    
    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(batch)
      });
    } catch (error) {
      console.error('Network error sending batch notifications:', error);
    }
  }
}

// Usage examples
const client = new JsonRpcClient('https://api.example.com/jsonrpc');

// Single notification
client.notify('user.activity', {
  userId: '12345',
  action: 'click',
  element: 'subscribe-button'
});

// Batch notifications
client.notifyBatch([
  { method: 'analytics.track', params: { event: 'page_view', page: '/home' }},
  { method: 'cache.invalidate', params: { key: 'homepage_data' }},
  { method: 'log.info', params: { message: 'User visited homepage' }}
]);

Python Client

import requests
import json
from typing import Any, Dict, List

class JsonRpcClient:
    def __init__(self, endpoint: str):
        self.endpoint = endpoint
        self.session = requests.Session()
    
    def notify(self, method: str, params: Any = None) -> None:
        """Send a notification (no response expected)"""
        notification = {
            'jsonrpc': '2.0',
            'method': method
        }
        
        if params is not None:
            notification['params'] = params
        
        try:
            self.session.post(
                self.endpoint,
                json=notification,
                timeout=5  # Short timeout for notifications
            )
        except requests.RequestException as e:
            # Only log network errors, don't raise
            print(f"Network error sending notification: {e}")
    
    def notify_batch(self, notifications: List[Dict[str, Any]]) -> None:
        """Send multiple notifications in batch"""
        batch = []
        for notif in notifications:
            notification = {
                'jsonrpc': '2.0',
                'method': notif['method']
            }
            if 'params' in notif:
                notification['params'] = notif['params']
            batch.append(notification)
        
        try:
            self.session.post(
                self.endpoint,
                json=batch,
                timeout=10
            )
        except requests.RequestException as e:
            print(f"Network error sending batch notifications: {e}")

# Usage examples
client = JsonRpcClient('https://api.example.com/jsonrpc')

# Single notification
client.notify('user.logout', {'userId': '12345', 'sessionId': 'abc123'})

# Batch notifications
client.notify_batch([
    {'method': 'analytics.track', 'params': {'event': 'logout'}},
    {'method': 'session.cleanup', 'params': {'sessionId': 'abc123'}},
    {'method': 'audit.log', 'params': {'action': 'user_logout', 'userId': '12345'}}
])

Node.js Server

const express = require('express');
const app = express();

app.use(express.json());

// Notification handlers
const notificationHandlers = {
  'log.event': async (params) => {
    console.log('Event logged:', params);
    // Store in analytics database
    await analytics.track(params);
  },
  
  'cache.invalidate': async (params) => {
    console.log('Invalidating cache:', params.keys);
    // Invalidate cache entries
    await cache.del(params.keys);
  },
  
  'email.send': async (params) => {
    console.log('Sending email to:', params.to);
    // Queue email for background processing
    await emailQueue.add('send-email', params);
  },
  
  'user.status.changed': async (params) => {
    console.log('User status changed:', params);
    // Broadcast to connected WebSocket clients
    websocketServer.broadcast('user.status.changed', params);
  }
};

// JSON-RPC handler
app.post('/jsonrpc', async (req, res) => {
  try {
    const request = req.body;
    
    // Handle batch requests
    if (Array.isArray(request)) {
      await handleBatchNotifications(request);
      // No response for notifications (even in batch)
      res.status(204).end();
      return;
    }
    
    // Single request
    if (isNotification(request)) {
      await handleNotification(request);
      // No response for notifications
      res.status(204).end();
    } else {
      // Handle regular request (with response)
      const result = await handleRequest(request);
      res.json(result);
    }
    
  } catch (error) {
    console.error('Error handling request:', error);
    // For notifications, still don't respond even on error
    if (isNotification(req.body)) {
      res.status(204).end();
    } else {
      res.json({
        jsonrpc: '2.0',
        error: { code: -32603, message: 'Internal error' },
        id: req.body.id || null
      });
    }
  }
});

function isNotification(request) {
  return request && !('id' in request);
}

async function handleNotification(notification) {
  const handler = notificationHandlers[notification.method];
  if (handler) {
    try {
      await handler(notification.params);
    } catch (error) {
      // Log error but don't respond (notifications are fire-and-forget)
      console.error(`Error handling notification ${notification.method}:`, error);
    }
  } else {
    console.warn(`Unknown notification method: ${notification.method}`);
  }
}

async function handleBatchNotifications(batch) {
  const promises = batch
    .filter(isNotification)
    .map(handleNotification);
  
  // Handle all notifications concurrently
  await Promise.allSettled(promises);
}

Best Practices

✅ Do

  • Use notifications for fire-and-forget operations
  • Batch multiple notifications to reduce network overhead
  • Handle errors gracefully on the server side without responding
  • Use short timeouts for notification requests
  • Log notification failures for debugging
  • Design your system to be resilient to lost notifications

❌ Don't

  • Send responses to notifications (violates the spec)
  • Use notifications for critical operations that require confirmation
  • Include an 'id' field in notifications
  • Expect error handling from the client side
  • Use notifications for operations that need return values
  • Rely on notifications for data consistency

WebSocket Integration

Notifications work particularly well with WebSocket connections for real-time communication.

WebSocket Notification Example

// Client-side WebSocket handler
const ws = new WebSocket('wss://api.example.com/websocket');

ws.onmessage = (event) => {
  const notification = JSON.parse(event.data);
  
  // Handle different notification types
  switch (notification.method) {
    case 'user.message.received':
      displayNewMessage(notification.params);
      break;
    case 'user.status.changed':
      updateUserStatus(notification.params);
      break;
    case 'system.maintenance.scheduled':
      showMaintenanceNotice(notification.params);
      break;
  }
};

// Server can send notifications like:
{
  "jsonrpc": "2.0",
  "method": "user.message.received",
  "params": {
    "messageId": "msg_123",
    "from": "alice",
    "content": "Hello there!",
    "timestamp": "2024-01-15T10:30:00Z"
  }
}