Add native WebSocket traffic tracking#171
Conversation
…riginal framed style
d5de2a0 to
bc8ec6e
Compare
Forced .thinMaterial/.visible fought the system nav bar transition, causing a large-title glitch on push. RequestDetailView never set this and pushes cleanly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WebSocketInterceptor attached a model to every task regardless of host, so ignoredHosts only ever worked for HTTP. Reuse CustomHTTPProtocol's suffix-match check before recording. Docs updated to reflect the shared behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two new tests: an ignored host skips model attachment, a non-ignored one still attaches. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Note: I've also started drafting a migration of the demo app to SwiftUI in my fork's |
pmusolino
left a comment
There was a problem hiding this comment.
Nice work @giobass , this is a big and useful feature! I think the swizzling approach makes sense for Wormholy because it keeps the host app code unchanged, but before merge I would tighten the memory story for long-running sockets and add one real send/receive capture test. After that, this feels much safer to maintain.
Small note on file headers: I am migrating the project to a simpler standard header, so for new files I prefer using:
// Copyright (c) 2026 Wormholy contributors
// SPDX-License-Identifier: MITNo big issue with the current Xcode-created header, just asking so new files follow the direction of the repo (which I will document in README).
|
Thanks again for all the updates. I did another pass over the PR and I think there are still a few issues that should be fixed before the merge. Once those are addressed, I will be happy to take another look. |
a47f992 to
4d5d9fe
Compare
|
@pmusolino I have addressed the other review comments. The remaining open points are the ones related to the delegate-proxy installation race condition and its test coverage. |
|
Thanks again for the updates! I did another pass and the previous feedback is addressed. The delegate-proxy race is still the same open issue; I also found one more edge case in the message limit and a performance issue from the new Base64 preview. I think we should fix these before merge. |
|
@pmusolino I addressed the remaining race-condition feedback and added the focused concurrent first-install coverage discussed above. |
| WebSocketConfiguration.messageLimit = nil | ||
| return | ||
| } | ||
| WebSocketConfiguration.messageLimit = NSNumber(value: Int(clamping: newValue.uint64Value)) |
There was a problem hiding this comment.
I think one value range still escapes this normalization. For example NSNumber(value: 1e20).uint64Value wraps before Int(clamping:), so it stores a lower value instead of Int.max as docs say. Can we compare with Int.max before converting to UInt64, then truncate only values inside the range?
| XCTAssertEqual(model.messages.map(\.text), ["first", "second"]) | ||
| } | ||
|
|
||
| func testOutOfRangeWebSocketMessageLimitIsClampedToIntMax() { |
There was a problem hiding this comment.
Current case covers UInt64.max, which converts correctly. Can we add one finite Double bigger than UInt64.max, for example 1e20? This will catch wrapping before the Int clamp.
| BOOL isClassMethod, | ||
| void (^ _Nonnull storeOriginal)(IMP _Nonnull originalImpl)); | ||
|
|
||
| #if DEBUG |
There was a problem hiding this comment.
This declaration disappears without DEBUG, but the Objective-C test file is still compiled in Release. Can we keep this in private test support for every test configuration, or guard the complete test with the same condition?
| WHReplaceMethod(selector, newImpl, affectedClass, isClassMethod, storeOriginal, nil); | ||
| } | ||
|
|
||
| #if DEBUG |
There was a problem hiding this comment.
Same for implementation: in Release this symbol is removed while test call remains. Can we make declaration, implementation and test use one consistent build condition?
| atomic_store_explicit(&whExampleForwardedCallCount, 0, memory_order_relaxed); | ||
|
|
||
| dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ | ||
| WormholyReplaceMethodStoringOriginalForTesting(@selector(makeSession), |
There was a problem hiding this comment.
This call is unconditional while helper declaration and implementation are DEBUG only, so a Release test build cannot compile or link this file. Please make the hook available to test target in all configurations, or wrap the complete test in the same guard.
|
Hopefully the last ones :D |
Goal
Add native WebSocket traffic interception to Wormholy alongside the existing HTTP request tracking, without requiring a third-party WebSocket library or changes to how the host app sends and receives messages.
Wormholy can capture
URLSessionWebSocketTasktraffic from both completion-handler andasync/awaitAPIs via method swizzling, and surface it in a new WebSockets tab next to Requests.How to test
The
WormholyDemoapp includes a WebSocket console connected to Postman's public WebSocket echo service.WormholyDemoon a simulator or device.Automated coverage runs through
WormholyDemoTests:xcodebuild test -scheme WormholyDemoThe Postman-backed integration tests are gated behind
WORMHOLY_RUN_NETWORK_TESTS=1, so standard CI remains deterministic. When enabled, they cover real completion-handler andasync/awaitsend/receive paths, plus delegate lifecycle forwarding and capture.What changed
sendandreceive, and provides a lightweightURLSessiondelegate proxy for real lifecycle callbacks.cancel(with:reason:)is intercepted on the Swift side.WebSocketInterceptorandWebSocketModel: installation and opt-in tracking logic, MainActor-isolated observable connection state, request/response headers, protocols, messages, close details, and errors.webSocketMessageLimitalso bounds pending message payloads before they reach the UI.Wormholy.limitapplies the same configured number independently to HTTP requests and WebSocket connections.RequestsViewnow supports Requests and WebSockets modes, with connection rows, detail inspection, message body views, and export.Wormholy.setWebSocketEnabled(_:)enables native WebSocket tracking independently of HTTP tracking.Wormholy.webSocketMessageLimitoptionally retains the newest N messages per connection.CustomHTTPProtocol: WebSocket upgrade requests are excluded from HTTP URLProtocol handling.WebSocketEchoClientand a console for end-to-end manual validation.Versioning
This is a backwards-compatible feature addition (new opt-in API, no breaking changes), so per semver it'd map to a 2.5.0 release. Leaving the actual Wormholy.podspec bump to you, since that's been handled as a separate release commit after merge in this repo's history.
Possible next step
The demo app (
WormholyDemo) is still UIKit + Storyboard. A follow-up worth considering is migrating it to SwiftUI, which would simplify wiring future demo features like this WebSocket console without touchingMain.storyboard.Limitations
didOpenanddidCloseevents require a delegate-backed session created while WebSocket tracking is enabled.URLSession.sharedstill captures messages but does not provide these lifecycle callbacks.WORMHOLY_RUN_NETWORK_TESTS=1.