How to Monkey-Patch the WebSocket Constructor for Network Interception
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, 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, the code stores the reference:
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:
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:
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:
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, the script uses originalAddEventListener with the third parameter set to true:
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:
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:
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:
window.postMessage({
source: 'websocket-proxy-injected',
payload: 'send',
data: event
}, '*');
The content script (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:
// Access from browser console
window.websocketProxyDebug.getConnectionCount();
window.websocketProxyDebug.connections.get('ws_main_1625390000000_1');
Code Examples
Example 1: Basic Monkey-Patching Pattern
// 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
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
// 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.WebSocketbefore patching to maintain access to native functionality. - Use capture-phase listeners: Register message listeners with the third parameter
trueto 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
Mapkeyed by connection IDs to track multiple concurrent WebSocket instances and their metadata. - Communicate safely: Use
window.postMessagewith 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →