How the WebSocket Message Simulation Feature Works in law-chain-hot/websocket-devtools

The message simulation feature injects artificial WebSocket traffic by invoking the native send method for outgoing messages and dispatching synthetic MessageEvent objects directly to user handlers for incoming messages.

The law-chain-hot/websocket-devtools extension enables developers to test WebSocket integrations without modifying server code or client applications. By leveraging the browser extension architecture—spanning the DevTools panel, background script, and injected content script—this feature manipulates live connections to simulate both client-to-server and server-to-client traffic.

Architecture of the Message Simulation System

The simulation flow traverses three distinct contexts: the React-based DevTools UI, the extension background script, and the page-injected content script that holds references to actual WebSocket instances.

Outgoing Simulation (Client-to-Server)

When simulating messages sent from the client, the extension bypasses its own proxy logic to deliver a genuine packet. In src/content/injected.js, the handleSimulateMessage function retrieves the saved original WebSocket.prototype.send method from connectionInfo.originalSend and invokes it directly on the live socket:

if (direction === "outgoing") {
  // Use the saved original send method, bypassing the proxy
  connectionInfo.originalSend.call(ws, message);
}

This approach ensures the server receives the message exactly as it would from legitimate user interaction, traversing the full network stack without interception or modification by the extension's monitoring layer.

Incoming Simulation (Server-to-Client)

For server-to-client simulation, the extension creates a synthetic event that bypasses the native WebSocket event pipeline entirely. The code constructs a MessageEvent object and dispatches it directly to the callbacks registered by the page's JavaScript:

} else if (direction === "incoming") {
  const simulatedEvent = new MessageEvent("message", {
    data: message,
    origin: connectionInfo.url,
    // … other standard fields …
  });
  simulatedEvent._isSimulated = true;

  // Directly invoke the user’s handlers
  if (connectionInfo.userOnMessage) {
    connectionInfo.userOnMessage.call(ws, simulatedEvent);
  }
  connectionInfo.userEventListeners.forEach(listener => {
    listener.call(ws, simulatedEvent);
  });
}

The _isSimulated flag marks the event for the DevTools UI, allowing the extension to distinguish injected traffic from genuine server responses.

Step-by-Step Implementation Flow

The message simulation feature operates through a coordinated sequence across the extension's architectural boundaries.

Step 1: Capturing Input in the DevTools Panel

The process initiates in src/components/SimulateMessagePanel.jsx when a user composes JSON and selects a direction. The handleSimulateMessage function validates the payload and transmits a Chrome runtime message:

await chrome.runtime.sendMessage({
  type: "simulate-message",
  data: {
    connectionId,
    message: messageData,
    direction, // "outgoing" or "incoming"
  },
});

Source: src/components/SimulateMessagePanel.jsx, lines 233–238.

Step 2: Relaying Through the Background Script

The background script (src/background/background.js) receives the simulate-message request and broadcasts it to the appropriate content script:

case "simulate-message": {
  const targetTabId = message.data.tabId || null;
  notifyAllTabs("simulate-message", message.data, targetTabId);
  sendResponse({ success: true, simulated: true });
  break;
}

Source: src/background/background.js, lines 200–206.

The notifyAllTabs utility forwards the payload to content scripts across browser tabs, isolating the target connection by connectionId.

Step 3: Executing Simulation in the Content Script

In src/content/content.js, the message handler receives the broadcast and invokes the page-injected logic. The actual execution occurs in src/content/injected.js within the handleSimulateMessage function (lines 35–102), which branches based on the direction parameter.

For outgoing messages (lines 48–61): The function calls connectionInfo.originalSend.call(ws, message), utilizing the stored reference to the native WebSocket.prototype.send method captured when the proxy first wrapped the socket.

For incoming messages (lines 65–99): The function instantiates a new MessageEvent("message", …) and sequentially invokes connectionInfo.userOnMessage followed by each listener in connectionInfo.userEventListeners. This direct invocation pattern ensures the page's event handlers execute without triggering the extension's own message interception logic.

Key Technical Implementation Details

Understanding the simulation feature requires familiarity with how the extension preserves and utilizes native WebSocket methods.

Preserving Native Send Methods

When the extension first proxies a WebSocket connection, it saves the original send method in connectionInfo.originalSend. This preservation is critical for outgoing simulation, as it allows the extension to transmit data through the genuine WebSocket channel rather than recursively triggering its own hooks.

Direct Handler Invocation Pattern

For incoming simulation, the extension maintains arrays of user-registered callbacks (userOnMessage and userEventListeners) separately from the native ws.onmessage property. By calling these functions directly with .call(ws, simulatedEvent), the extension mimics the standard this binding and event interface while avoiding the browser's internal event propagation mechanism.

Code Example: Triggering Simulation from Custom Scripts

While the primary interface is the DevTools panel, understanding the message structure enables programmatic interaction:

// Pattern from SimulateMessagePanel.jsx
const handleSimulateMessage = async (direction, data = null) => {
  const payload = data || simulateMessage;   // JSON string from user input

  if (!connection || !payload.trim() || isSending) return;

  setIsSending(true);
  try {
    await onSimulateMessage({
      connectionId: connection.id,
      message: payload,
      direction,                     // "outgoing" or "incoming"
    });
  } finally {
    setTimeout(() => setIsSending(false), 200);
  }
};

// UI binding
<button onClick={() => handleSimulateMessage('outgoing')}>Send → Server</button>
<button onClick={() => handleSimulateMessage('incoming')}>Inject ← Server</button>

The onSimulateMessage callback originates in src/devtools/panel.jsx (lines 787–801) and initiates the Chrome runtime messaging sequence.

Summary

  • Outgoing simulation transmits data by invoking the preserved native WebSocket.prototype.send method directly on the live socket, ensuring server-side authenticity.
  • Incoming simulation constructs synthetic MessageEvent objects and dispatches them directly to user-registered handlers, bypassing the native event pipeline.
  • The _isSimulated flag identifies injected messages in the DevTools message history.
  • Core implementation resides in src/content/injected.js, coordinated through src/background/background.js and triggered from src/components/SimulateMessagePanel.jsx.

Frequently Asked Questions

How does the extension prevent simulated messages from being intercepted by its own proxy?

For outgoing messages, the extension calls connectionInfo.originalSend—a reference to the native WebSocket.prototype.send method captured before the proxy was applied—ensuring the message bypasses the extension's instrumentation. For incoming messages, the extension invokes user handlers directly rather than assigning to ws.onmessage, circumventing the proxy's setter hooks entirely.

Can simulated server-to-client messages trigger the browser's native WebSocket events?

No. The simulation creates a MessageEvent object but dispatches it by directly calling connectionInfo.userOnMessage and iterating through connectionInfo.userEventListeners. This approach skips the browser's internal event propagation mechanism, meaning addEventListener callbacks registered after the WebSocket was proxied will still fire, but the event never traverses the native WebSocket event pipeline.

What identifies a message as simulated in the DevTools interface?

The extension sets simulatedEvent._isSimulated = true on synthetic MessageEvent objects before dispatching them to user handlers. This flag propagates through the message history tracking system (wsHistoryService.js), allowing the UI to render a "simulated" badge next to injected messages in the Message Details view.

Is the message simulation feature available for all WebSocket connections?

The feature requires the extension to have successfully proxied the WebSocket instance in the target page. As implemented in law-chain-hot/websocket-devtools, the simulation only works on connections established after the DevTools panel opened and while the panel remains active, as the connectionInfo object maintaining references to originalSend and user handlers is managed by the content script's connection tracking logic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →