Network.framework: Happy Eyeballs cancels also-ran only after WebSocket handshake (duplicate WS sessions)
Hi everyone 👋
When using NWConnection with NWProtocolWebSocket, I’ve noticed that Happy Eyeballs cancels the losing connection only after the WebSocket handshake completes on the winning path.
As a result, both IPv4 and IPv6 attempts can send the GET / Upgrade request in parallel, which may cause duplicate WebSocket sessions on the server.
Standards context
RFC 8305 §6 (Happy Eyeballs v2) states:
Once one of the connection attempts succeeds (generally when the TCP
handshake completes), all other connections attempts that have not
yet succeeded SHOULD be canceled.
This “SHOULD” is intentionally non-mandatory — implementations may reasonably delay cancellation to account for additional factors (e.g. TLS success or ALPN negotiation).
So Network.framework’s current behavior — canceling after the WebSocket handshake — is technically valid, but it can have practical side effects at the application layer.
Why this matters
WebSocket upgrades are semantically HTTP GET requests
(RFC 6455 §4.1).
Per RFC 9110 §9.2,
GET requests are expected to be safe and idempotent — they should not have side effects on the server.
In practice, though, WebSocket upgrades often:
include Authorization headers or cookies
create authenticated or persistent sessions
So if both IPv4 and IPv6 paths reach the upgrade stage, the server may create duplicate sessions before one connection is canceled.
Questions / Request
Is there a way to make Happy Eyeballs cancel the losing path earlier — for example, right after TCP or TLS handshake — when using NWProtocolWebSocket?
If not, could Apple consider adding an option (e.g. in NWProtocolWebSocket.Options) to control the cancellation threshold, such as:
after TCP handshake
after TLS handshake
after protocol handshake (current behavior)
That would align the implementation more closely with RFC 8305 and help prevent duplicate, non-idempotent upgrade requests.
Context
I’m aware of Quinn’s post Understanding Also-Ran Connections.
This report focuses specifically on the cancellation timing for NWProtocolWebSocket and the impact of duplicate upgrade requests.
Although RFC 6455 and RFC 9110 define WebSocket upgrades as safe and idempotent HTTP GETs, in practice they often establish authenticated or stateful sessions.
Thus, delaying cancellation until after the upgrade can create duplicate sessions — even though the behavior is technically RFC-compliant.
Happy to share a sysdiagnose and sample project via Feedback if helpful.
Thanks! 🙏
Example log output
With Network Link Conditioner (Edge):
log stream --info --predicate 'subsystem == "com.apple.network" && process == "WS happy eyeballs"'
2025-11-03 17:02:48.875258 [C3] create connection to wss://echo.websocket.org:443
2025-11-03 17:02:48.878949 [C3.1] starting child endpoint 2a09:8280:1::37:b5c3:443 # IPv6
2025-11-03 17:02:48.990206 [C3.1] starting child endpoint 66.241.124.119:443 # IPv4
2025-11-03 17:03:00.251928 [C3.1.1] Socket received CONNECTED event # IPv6 TCP up
2025-11-03 17:03:00.515837 [C3.1.2] Socket received CONNECTED event # IPv4 TCP up
2025-11-03 17:03:04.543651 [C3.1.1] Output protocol connected (WebSocket) # WS ready on IPv6
2025-11-03 17:03:04.544390 [C3.1.2] nw_endpoint_handler_cancel # cancel IPv4 path
2025-11-03 17:03:04.544913 [C3.1.2] TLS warning: close_notify # graceful close IPv4
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Hi everyone,
I’m running into an issue with PHPickerFilter when using PHPickerViewController.
When I configure the picker with a .videos and .livePhotos filter, it seems to work correctly in the Photos tab. However, when I switch to the Collections tab, the filter doesn’t always apply — users can still see and select static image assets in certain collections (e.g. from one of the People & Pets sections).
Here’s a simplified snippet of my setup:
var configuration = PHPickerConfiguration(photoLibrary: .shared())
configuration.selectionLimit = 1
var filters = [PHPickerFilter]()
filters.append(.videos)
filters.append(.livePhotos)
configuration.filter = PHPickerFilter.any(of: filters)
configuration.preferredAssetRepresentationMode = .current
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
present(picker, animated: true)
Expected behavior:
The picker should consistently respect the filter across both Photos and Collections tabs, only showing assets that match the filter.
Actual behavior:
The filter seems to apply correctly in the Photos tab, but in the Collections tab, other asset types are still visible/selectable.
Has anyone else encountered this behavior? Is this expected or a known issue, or am I missing something in the configuration?
Thanks in advance!
Topic:
Media Technologies
SubTopic:
Photos & Camera
Tags:
Files and Storage
Media Library
Photos and Imaging
PhotoKit
I use AVPlayerItemMetadataOutput for live HLS audio stream, each segment is a 0.96 second long AAC containing id3 metadata.
In all previous versions of iOS, the AVPlayerItemMetadataOutput delegate method for this stream is called approximately every 0.96 seconds.
This behaviour has changed with iOS 15.4.1. In this version, the delegate method is called exactly every 1 second, resulting in a delay in reading the metadata for each segment.
Example:
time(sec)----|0___________1___________2___________3______
segments-----|[segment_1][segment_2][segment_3][segment_4]
|^----------^----------^----------^---------
iOS 15.2-----|call_1 call_2 call_3 call_4
|^-----------^-----------^-----------^------
iOS 15.4.1---|call_1 call_2 call_3 call_4
As it can be seen, call_4 will be called much later than segment_4 starts playing. In all previous versions of iOS, it was called simultaneously with the start of segment_4 playback.
The AVMetadataItem.time property also shows the wrong time (see attached pictures).
Tried adjusting the delegation in both main and background queue - no success. Changing advanceIntervalForDelegateInvocation did not change this behavior.