Repository navigation
Support URLSession's new loader on iOS 26 and iOS 27 - #195
Merged
Merged
Conversation
- Capture traffic from URLSession's Network.framework loader (NWURLSession), used when usesClassicLoadingMode is false (iOS 18.4+) and required by enablesEarlyData (HTTP/3 0-RTT) on iOS 26 - Capture the request body of async upload(for:from:) and upload(for:fromFile:) - Keep WebSocket messages sent before the handshake, in order - Install the resume hook on watchOS 10/11 - Add tests for both loaders with an in-process HTTP/WebSocket server Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Each tab lists the URLSession APIs that Atlantis captures, grouped, with the method that Atlantis swizzles for each one - iOS 18: the classic loader. iOS 26: the new loader, Early Data and Ultra Constrained networks. iOS 27: AsyncImage(request:) and asyncImageURLSession(_:) - Tap an API or Run All: each row shows the status code and whether Atlantis captured the request (AtlantisDelegate) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
macOS 15 / iOS 18 don't have -[NWURLSessionWebSocketTask completeTaskWithError:retryable:], so a rejected upgrade on the new loader wasn't captured (CI on macOS 15). The receive completion now reports it, and the completeTaskWithError:retryable: hook is optional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
httpbin.proxyman.app/bytes/N fails (502, or a reset HTTP/2 stream with an empty body), so the test failed on CI. /image/png works. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Each row is the title and the method that Atlantis swizzles, with a small status badge. A green ✓ means Atlantis captured the request - Remove the header, the section footers and the extra labels - Load the Atlantis logo instead of the httpbin sample images Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Keep WebSocket messages sent before the handshake on the task, and send them after the handshake traffic. The package is built from the final request (headers, scheme), and the messages are released with the task - Drop the waiting messages and the package if the handshake fails - Queue messages sent before resume(), only drop them once the WS is closed - Encode a WS message with its package without mutating the package, and stop touching shared state in startSendingWebsocketMessage (gRPC calls it from any thread) - Report each new-loader data chunk before calling the original, so the next chunk can't overtake it - Report a failed handshake from the send/ping completion handlers on iOS 18 / macOS 15, and make the per-message handshake check lock-free once it's reported - Share a markOnce helper for the once-per-task reports - Skip reading upload files larger than the maximum package size - Check completeTaskWithError:retryable: whenever it exists in the tests - Don't let the SSE demo server wait forever in .waiting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The iOS 18 and iOS 26 tabs connect to echo.websocket.org with URLSessionWebSocketTask, and each row calls one API on the same connection: Connect, Send Text, Send Data, Receive, Ping and Close. The connection keeps receiving like an app does. URLSession reads a pong only after the messages before it, and the classic loader only while a receive is pending, so Ping timed out without the receive loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
enablesEarlyData, iOS 26.1 addsallowsUltraConstrainedNetworkAccess.usesClassicLoadingMode = false(iOS 18.4+), andenablesEarlyDatarequires it. Apple's header says the default "may be NO in a future OS update" (it's still the classic loader on iOS 27.0 and macOS 27.0.1).NWURLSessionTask(not a subclass ofNSURLSessionTask) and none of Atlantis's hooks fired: HTTP, upload, download, async/await and WebSocket traffic were all missed.Changes
NetworkInjector+NWURLSession.swift):-[NWURLSessionTask resume]and the upload methods ofNWURLSession-[NWURLSessionResponseConsumer* task:deliverResponse:completionHandler:]andtask:deliverData:complete:error:completionHandler:NWURLSessionWebSocketTask; the handshake fromloaderConnectedWithHTTPConnectionMetadata:CNAMEChain:unlistedTracker:, failures fromcompleteTaskWithError:retryable:upload(for:from:)andupload(for:fromFile:)was missing (they use the private_uploadTaskWithRequest:…delegate:completionHandler:)resumehook wasn't installed on watchOS 10/11assertionFailureAtlantis.xcodeproj(Carthage)Example app
AsyncImage(request:),.asyncImageURLSession(_:))AtlantisDelegate)URLSessionWebSocketTask(echo.websocket.org)Testing
URLSessionLoaderTests.swift: 23 scenarios run on both loaders (URLSessionCaptureTests,NetworkLoaderCaptureTests), plusURLSessionPrivateAPITeststhat check the private classes, selectors and type encodings. They use an in-process HTTP/SSE/WebSocket server (LocalTestServer.swift), so no internet or Node.js is needed.swift test): 60 passed, 1 skippedbuild-for-testingsucceeds for iOS, tvOS, watchOS (simulator and arm64_32 device), visionOS, macOS and Mac CatalysttestDownloadRequestCapturedusedhttpbin.proxyman.app/bytes/32, which fails (502, or a reset HTTP/2 stream with an empty body). It downloads/image/pngnow (df36b7b).Notes
-[NWURLSessionWebSocketTask completeTaskWithError:retryable:]. It's optional now: a failed handshake is also reported from the receive hook (6c3daae).Atlantis.xcodeproj(Carthage) doesn't build with Xcode 27 because its deployment targets (iOS 13, macOS 10.15, tvOS 13) are below Xcode 27's minimum. That's not changed here.🤖 Generated with Claude Code