# How to Monkey-Patch the WebSocket Constructor for Network Interception

> Discover how law-chain-hot/websocket-devtools monkey-patches the WebSocket constructor for network interception by wrapping the native class and overriding methods to capture all traffic.

- Repository: [Brian 阿布/websocket-devtools](https://github.com/law-chain-hot/websocket-devtools)
- Tags: internals
- Published: 2026-03-05

---

**The extension saves the native `WebSocket` constructor, wraps it in a `ProxiedWebSocket` class that registers capture-phase listeners and overrides methods like `send`, then replaces `window.WebSocket` to intercept all traffic before it reaches user code.**

The `law-chain-hot/websocket-devtools` extension enables real-time WebSocket monitoring by injecting a script that monkey-patches the browser's native `WebSocket` constructor. This technique, implemented in [`src/content/injected.js`](https://github.com/law-chain-hot/websocket-devtools/blob/main/src/content/injected.js), creates a transparent interception layer that can observe, block, and log all WebSocket activity without modifying application code.

## The Three-Phase Monkey-Patching Architecture

The interception strategy follows a careful three-phase approach to ensure the original functionality remains intact while adding observability.

### Phase 1: Preserving the Native Constructor

Before any modifications occur, the script captures the genuine `WebSocket` constructor to prevent irreversible changes. At lines 15–17 of [`src/content/injected.js`](https://github.com/law-chain-hot/websocket-devtools/blob/main/src/content/injected.js), the code stores the reference:

```javascript
const OriginalWebSocket = window.WebSocket;

```

This preservation is critical because the proxy needs to instantiate real WebSocket connections internally. The script also sets a guard flag at lines 8–13 to prevent duplicate injection:

```javascript
if (window.websocketProxyInjected) {
  console.log('[WebSocket Proxy] Already injected, skipping');
  return;
}
window.websocketProxyInjected = true;

```

### Phase 2: Constructing the Proxy Wrapper

Starting at line 84, the script defines the `ProxiedWebSocket` constructor. This wrapper generates unique connection IDs via `generateConnectionId()` to track multiple sockets across frames, then instantiates a real socket using the saved constructor:

```javascript
function ProxiedWebSocket(url, protocols) {
  const ws = new OriginalWebSocket(url, protocols);
  const connectionId = generateConnectionId();
  // ... interception logic
}

```

The wrapper maintains internal state in a `connections` Map (lines 96–109) that stores per-socket metadata including the original methods, blocked message queues, and user listener arrays.

### Phase 3: Replacing the Global Reference

Finally, at lines 105–112, the script overwrites `window.WebSocket` with the proxy:

```javascript
window.WebSocket = ProxiedWebSocket;

```

This replacement ensures that all subsequent `new WebSocket()` calls in the page context return proxied instances capable of interception.

## Intercepting Messages with Capture-Phase Listeners

To intercept incoming messages before user code receives them, the proxy registers a **capture-phase** listener on the native socket. At line 119 in [`src/content/injected.js`](https://github.com/law-chain-hot/websocket-devtools/blob/main/src/content/injected.js), the script uses `originalAddEventListener` with the third parameter set to `true`:

```javascript
originalAddEventListener.call(ws, 'message', ourMessageListener, true);

```

This guarantees that `ourMessageListener` executes before any user-registered listeners. Inside this handler, the script checks `proxyState.blockIncoming` to determine whether to drop the message or forward it:

```javascript
if (proxyState.blockIncoming && proxyState.isMonitoring) {
  connectionInfo.blockedMessages.push({ 
    data: event.data, 
    timestamp: Date.now() 
  });
  sendEvent({ ...event, blocked: true, reason: 'Incoming messages blocked' });
  return; // Prevent propagation to user code
}

```

## Overriding Critical WebSocket Methods

The proxy overrides key methods to ensure all traffic flows through the monitoring layer.

### The Send Method Interception

At lines 122–149, the wrapper replaces the socket's `send` method to log outgoing data. The override processes binary data, checks `proxyState.blockOutgoing`, and conditionally calls the original:

```javascript
ws.send = function(data) {
  const binaryInfo = processMessageWithBinary(data);
  const event = {
    id: connectionId,
    url: ws.url,
    type: 'message',
    direction: 'outgoing',
    data: data,
    ...binaryInfo,
    messageId: generateMessageId(),
    timestamp: Date.now()
  };
  
  sendEvent(event); // Forward to DevTools
  
  if (!proxyState.blockOutgoing) {
    return originalSend.call(ws, data);
  }
};

```

### Event Listener Management

Lines 151–179 override `addEventListener`, `removeEventListener`, and the `onmessage` property. Rather than registering user callbacks directly on the native socket, the proxy stores them in `connectionInfo.userEventListeners`. This ensures the capture-phase listener maintains priority and can filter messages before invoking user handlers.

## Communication with the DevTools Extension

All intercepted data flows from the injected script to the extension via `window.postMessage`. The script uses a specific source identifier `"websocket-proxy-injected"` (visible at lines 98–101) to distinguish its messages:

```javascript
window.postMessage({
  source: 'websocket-proxy-injected',
  payload: 'send',
  data: event
}, '*');

```

The content script ([`src/content/content.js`](https://github.com/law-chain-hot/websocket-devtools/blob/main/src/content/content.js)) receives these messages, adds unique `messageId` values and frame context metadata, then forwards them to the background script and DevTools panel for display.

## Debugging and Runtime Introspection

For development purposes, the script exposes `window.websocketProxyDebug` at lines 233–260. This object provides access to the internal `connections` Map and proxy state:

```javascript
// Access from browser console
window.websocketProxyDebug.getConnectionCount();
window.websocketProxyDebug.connections.get('ws_main_1625390000000_1');

```

## Code Examples

### Example 1: Basic Monkey-Patching Pattern

```javascript
// Save original before modification
const OriginalWebSocket = window.WebSocket;

// Create proxy constructor
function ProxiedWebSocket(url, protocols) {
  const ws = new OriginalWebSocket(url, protocols);
  
  // Add interception logic here
  console.log(`WebSocket connecting to: ${url}`);
  
  return ws;
}

// Replace global reference
window.WebSocket = ProxiedWebSocket;

```

### Example 2: Intercepting Outgoing Messages

```javascript
const originalSend = WebSocket.prototype.send;
WebSocket.prototype.send = function(data) {
  // Log before sending
  console.log('Outgoing:', data);
  
  // Call original with same context and arguments
  return originalSend.apply(this, arguments);
};

```

### Example 3: Blocking Incoming Traffic Conditionally

```javascript
// Inside the proxy's message listener
ws.addEventListener('message', function(event) {
  if (shouldBlockMessage(event.data)) {
    console.log('Blocked:', event.data);
    event.stopImmediatePropagation(); // Prevent user handlers
    return;
  }
  // Forward to user listeners...
}, true); // Capture phase ensures priority

```

## Summary

- **Preserve the original**: Always store `window.WebSocket` before patching to maintain access to native functionality.
- **Use capture-phase listeners**: Register message listeners with the third parameter `true` to intercept before application code receives events.
- **Override methods carefully**: Wrap `send`, `addEventListener`, and property setters to route traffic through your monitoring layer.
- **Maintain state**: Use a `Map` keyed by connection IDs to track multiple concurrent WebSocket instances and their metadata.
- **Communicate safely**: Use `window.postMessage` with a unique source identifier to bridge the isolated worlds of the injected script and content script.

## Frequently Asked Questions

### How does the extension prevent duplicate script injection?

The script checks for `window.websocketProxyInjected` immediately upon execution (lines 8–13). If the flag exists, the script exits early to avoid re-patching an already wrapped `WebSocket` constructor, which would create nested proxy layers and break references.

### Can the proxy block individual messages or only all traffic?

The implementation supports granular blocking through `proxyState.blockIncoming` and `proxyState.blockOutgoing` booleans. When blocking is enabled, messages are stored in `connectionInfo.blockedMessages` arrays (visible in the `connections` Map) and logged with a `blocked: true` flag before being dropped, allowing the DevTools UI to display exactly what was filtered.

### Why does the proxy use capture-phase event listeners?

The script calls `originalAddEventListener("message", handler, true)` at line 119 to register in the capture phase. This ensures the proxy's `ourMessageListener` executes before any bubble-phase listeners registered by user code, enabling the extension to intercept, log, or block messages before the application processes them.

### How can developers inspect the internal proxy state at runtime?

The script exposes `window.websocketProxyDebug` (lines 233–260), which provides methods like `getConnectionCount()` and access to the private `connections` Map. Developers can call these from the browser console to view active sockets, queued blocked messages, and current proxy configuration flags.