Skip to content

Add WebSocket traffic support to HAR generator and per-message mocking#1740

Open
waldekmastykarz wants to merge 3 commits into
dotnet:nextfrom
waldekmastykarz:waldekmastykarz-websocket-har-support
Open

Add WebSocket traffic support to HAR generator and per-message mocking#1740
waldekmastykarz wants to merge 3 commits into
dotnet:nextfrom
waldekmastykarz:waldekmastykarz-websocket-har-support

Conversation

@waldekmastykarz

Copy link
Copy Markdown
Collaborator

Summary

Adds two major WebSocket capabilities to Dev Proxy:

  1. HAR generator captures WebSocket messages — WebSocket traffic relayed through the proxy is now recorded in HAR files following the Chrome DevTools / mitmproxy convention (_resourceType: "websocket" + _webSocketMessages array on the upgrade entry).

  2. Per-message WebSocket interception — Converts the WebSocket mock plugin from full-connection takeover to per-message interception with origin passthrough, matching how HTTP mocking works: matched messages get mock responses, unmatched ones pass through to the origin.

Changes

WebSocket message capture (HAR)

  • Add WebSocketMessageRecord abstraction (direction, type, data, timestamp)
  • Add WebSocketMessages property to IProxySession
  • Replace raw byte relay in WebSocketRelay with WebSocket.CreateFromStream-based frame-level relay that captures every message
  • Add PrefixedStream to handle leftover handshake bytes
  • Add _resourceType and _webSocketMessages HAR extension fields (matching Chrome/mitmproxy convention)
  • Populate WebSocket messages in HarGeneratorPlugin for upgrade entries

Per-message interception

  • Add InterceptWebSocketMessages(interceptor, onConnected) to IProxySession
  • Split relay into RelayClientToOriginAsync and RelayOriginToClientAsync with SemaphoreSlim for thread-safe client sends
  • Add InterceptorClientConnection wrapper with send serialization and message capture
  • Convert WebSocketMockResponsePlugin from HandleWebSocket to InterceptWebSocketMessages
  • Add origin-unreachable fallback: when the origin cannot be reached but an interceptor is registered, the proxy falls back to mock-only mode (same UX as before, just automatic)

Architecture

With origin reachable:
  Client ──msg──▶ Proxy ──interceptor──▶ Origin
                    │ matched? mock response ◀──┘
                    │ unmatched? forward to origin

With origin unreachable (fallback):
  Client ──msg──▶ Proxy (mock server)
                    │ interceptor handles matched messages
                    │ unmatched messages are dropped

Testing

  • All 306 tests pass (0 failures)
  • WebSocketRelayTests rewritten for frame-level relay with message capture verification
  • Both WebSocketMockIntegrationTests pass (mock-only fallback covers the no-origin scenario)

waldekmastykarz and others added 2 commits July 2, 2026 11:23
Replace raw byte WebSocket relay with frame-aware relay using
WebSocket.CreateFromStream on both client and origin sides. Each
relayed message is captured as a WebSocketMessageRecord and stored
on the session.

The HAR generator now detects WebSocket upgrade entries and embeds
captured messages using the _resourceType and _webSocketMessages
custom HAR fields, following the Chrome DevTools / mitmproxy
convention.

Changes:
- Add WebSocketMessageRecord abstraction (direction, type, data,
  timestamp)
- Add IProxySession.WebSocketMessages property
- Convert WebSocketRelay from raw byte splice to message-level
  relay with PrefixedStream for leftover handshake bytes
- Add HarWebSocketMessage model and _resourceType/_webSocketMessages
  fields to HarEntry
- Update HarGeneratorPlugin to populate WebSocket fields
- Update WebSocketRelayTests for frame-level relay and message
  capture verification

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Convert WebSocket mocking from full-connection takeover to per-message
interception: the proxy connects to the origin and relays traffic, but
each client message is offered to the interceptor first. Matched
messages get mock responses; unmatched ones pass through to the origin.

When the origin is unreachable but an interceptor is registered, the
proxy falls back to mock-only mode — same as the old HandleWebSocket
behavior.

- Add InterceptWebSocketMessages to IProxySession with interceptor and
  onConnected callbacks
- Split WebSocket relay into client-to-origin and origin-to-client tasks
  with thread-safe client sends via SemaphoreSlim
- Add InterceptorClientConnection wrapper with send serialization and
  message capture for HAR
- Convert WebSocketMockResponsePlugin from HandleWebSocket to
  InterceptWebSocketMessages
- Add origin-unreachable fallback in ProxyConnectionHandler

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds WebSocket support across the Kestrel proxy engine and plugins by (1) capturing relayed WebSocket messages for downstream reporting/HAR generation, and (2) enabling per-message WebSocket interception to support HTTP-like selective mocking with origin passthrough (plus a mock-only fallback when the origin is unreachable).

Changes:

  • Implement frame-level WebSocket relay using WebSocket.CreateFromStream, with message capture and optional per-message interception.
  • Extend IProxySession / CanonicalProxySession to expose captured WebSocketMessages and register WebSocket message interceptors.
  • Extend HAR generation/models with Chrome/mitmproxy-style WebSocket extension fields (_resourceType, _webSocketMessages) and convert WebSocketMockResponsePlugin to per-message interception.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
DevProxy.Proxy.Kestrel/Internal/WebSocketRelay.cs Replaces raw stream splicing with WebSocket message relay + capture + optional interceptor path.
DevProxy.Proxy.Kestrel/Internal/ProxyConnectionHandler.cs Wires relay capture/interception into the request pipeline and adds interceptor-only fallback mode.
DevProxy.Proxy.Kestrel/Http/CanonicalProxySession.cs Stores captured WebSocket messages and exposes interceptor registration + callbacks.
DevProxy.Proxy.Kestrel.Tests/WebSocketRelayTests.cs Updates relay test to use real WebSocket frames and validates message capture.
DevProxy.Plugins/Models/Har.cs Adds HAR extension fields and HarWebSocketMessage model for WebSocket message export.
DevProxy.Plugins/Mocking/WebSocketMockResponsePlugin.cs Converts WebSocket mocking from full takeover to per-message interception with passthrough.
DevProxy.Plugins/Generation/HarGeneratorPlugin.cs Emits WebSocket HAR extensions for upgrade requests when captured messages are present.
DevProxy.Abstractions/Proxy/Http/WebSocketMessageRecord.cs Introduces captured-message record abstraction (direction/type/data/timestamp).
DevProxy.Abstractions/Proxy/Http/IProxySession.cs Adds WebSocketMessages and InterceptWebSocketMessages(...) to the plugin-facing session API.

Comment thread DevProxy.Plugins/Generation/HarGeneratorPlugin.cs Outdated
Comment thread DevProxy.Abstractions/Proxy/Http/IProxySession.cs Outdated
Comment thread DevProxy.Proxy.Kestrel/Internal/ProxyConnectionHandler.cs
Comment thread DevProxy.Proxy.Kestrel/Internal/ProxyConnectionHandler.cs
Comment thread DevProxy.Plugins/Generation/HarGeneratorPlugin.cs
- Map WebSocketMessageType to RFC 6455 opcodes (1=text, 2=binary,
  8=close) in HAR output instead of the framework enum values (0/1/2)
- Only capture WebSocket messages for watched requests to avoid
  unbounded memory growth on long-lived/high-volume sockets
- Capture interceptor/onConnected responses in the interceptor-only
  fallback so mock-only HAR output includes receive entries
- Update IProxySession.WebSocketMessages doc to reflect interception
  and mock-only fallback capture
- Add HarGenerator_WritesWebSocketMessages test asserting correct
  type/time/opcode/data formatting

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Comment on lines 118 to 122
if (statusCode != (int)HttpStatusCode.SwitchingProtocols)
{
// Origin declined the upgrade. We've relayed its response; there's no
// tunnel to splice. Close (a non-101 may carry a body we don't frame yet).
logger.LogDebug("WebSocket origin {Host} declined upgrade with {Status}", origin.Host, statusCode);
Comment on lines +578 to +595
public async Task CloseAsync(CancellationToken cancellationToken)
{
await sendLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (clientWs.State is WebSocketState.Open or WebSocketState.CloseReceived)
{
await clientWs.CloseOutputAsync(
WebSocketCloseStatus.NormalClosure, statusDescription: null, cancellationToken).ConfigureAwait(false);
}
}
catch (WebSocketException) { }
catch (OperationCanceledException) { }
finally
{
sendLock.Release();
}
}
Comment on lines +627 to +628
public Task CloseAsync(CancellationToken cancellationToken) =>
inner.CloseAsync(cancellationToken);
Comment on lines +659 to +663
var msg = await connection.ReceiveAsync(ct).ConfigureAwait(false);
if (msg is null || msg.Type == WebSocketMessageType.Close)
{
break;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants