Add WebSocket traffic support to HAR generator and per-message mocking#1740
Open
waldekmastykarz wants to merge 3 commits into
Open
Add WebSocket traffic support to HAR generator and per-message mocking#1740waldekmastykarz wants to merge 3 commits into
waldekmastykarz wants to merge 3 commits into
Conversation
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>
Contributor
There was a problem hiding this comment.
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/CanonicalProxySessionto expose capturedWebSocketMessagesand register WebSocket message interceptors. - Extend HAR generation/models with Chrome/mitmproxy-style WebSocket extension fields (
_resourceType,_webSocketMessages) and convertWebSocketMockResponsePluginto 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. |
- 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>
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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds two major WebSocket capabilities to Dev Proxy:
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"+_webSocketMessagesarray on the upgrade entry).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)
WebSocketMessageRecordabstraction (direction, type, data, timestamp)WebSocketMessagesproperty toIProxySessionWebSocketRelaywithWebSocket.CreateFromStream-based frame-level relay that captures every messagePrefixedStreamto handle leftover handshake bytes_resourceTypeand_webSocketMessagesHAR extension fields (matching Chrome/mitmproxy convention)HarGeneratorPluginfor upgrade entriesPer-message interception
InterceptWebSocketMessages(interceptor, onConnected)toIProxySessionRelayClientToOriginAsyncandRelayOriginToClientAsyncwithSemaphoreSlimfor thread-safe client sendsInterceptorClientConnectionwrapper with send serialization and message captureWebSocketMockResponsePluginfromHandleWebSockettoInterceptWebSocketMessagesArchitecture
Testing
WebSocketRelayTestsrewritten for frame-level relay with message capture verificationWebSocketMockIntegrationTestspass (mock-only fallback covers the no-origin scenario)