NetworkConnection throws EINVAL when receiving ping/pong control frames

Summary

NetworkConnection<WebSocket> in iOS 26 Network framework throws POSIXErrorCode(rawValue: 22): Invalid argument when receiving WebSocket ping (opcode 9) or pong (opcode 10) control frames. This prevents proper WebSocket keep-alive functionality.

Environment

  • iOS 26.0 (Simulator)
  • macOS 26.1
  • Xcode 26.0

Note: This issue was initially discovered on iOS 26 Simulator. The same behavior was confirmed on macOS 26, suggesting a shared bug in the Network framework. The attached sample code is for macOS for easier reproduction.

Description

When using the new NetworkConnection<WebSocket> API introduced in iOS 26 or macOS 26, the receive() method throws EINVAL error whenever a ping or pong control frame is received from the server.

This is a critical issue because:

  1. WebSocket servers commonly send ping frames to keep connections alive
  2. Clients send ping frames to verify connection health
  3. The receive callback never receives the ping/pong frame - the error occurs before the frame reaches user code

Steps to Reproduce

  1. Create a WebSocket connection to any server that supports ping/pong (e.g., wss://echo.websocket.org):
import Foundation
import Network

// MARK: - WebSocket Ping/Pong EINVAL Bug Reproduction
// This sample demonstrates that NetworkConnection<WebSocket> throws EINVAL
// when receiving ping or pong control frames.

@main
struct WebSocketPingPongBug {
    static func main() async {
        print("=== WebSocket Ping/Pong EINVAL Bug Reproduction ===\n")

        do {
            try await testPingPong()
        } catch {
            print("Test failed with error: \(error)")
        }
    }

    static func testPingPong() async throws {
        let host = "echo.websocket.org"
        let port: UInt16 = 443

        print("Connecting to wss://\(host)...")

        let endpoint = NWEndpoint.hostPort(
            host: NWEndpoint.Host(host),
            port: NWEndpoint.Port(rawValue: port)!
        )

        try await withNetworkConnection(to: endpoint, using: {
            WebSocket {
                TLS {
                    TCP()
                }
            }
        }) { connection in
            print("Connected!\n")

            // Start receive loop in background
            let receiveTask = Task {
                var messageCount = 0
                while !Task.isCancelled {
                    do {
                        let (data, metadata) = try await connection.receive()
                        messageCount += 1

                        print("[\(messageCount)] Received frame - opcode: \(metadata.opcode)")

                        if let text = String(data: data, encoding: .utf8) {
                            print("[\(messageCount)] Content: \(text)")
                        } else {
                            print("[\(messageCount)] Binary data: \(data.count) bytes")
                        }
                    } catch let error as NWError {
                        if case .posix(let code) = error, code == .EINVAL {
                            print("❌ EINVAL error occurred! (POSIXErrorCode 22: Invalid argument)")
                            print("   This is the bug - ping/pong frame caused EINVAL")
                            // Continue to demonstrate workaround
                            continue
                        }
                        print("Receive error: \(error)")
                        break
                    } catch {
                        print("Receive error: \(error)")
                        break
                    }
                }
            }

            // Wait for initial message from server
            try await Task.sleep(for: .seconds(2))

            // Test 1: Send text message (should work)
            print("\n--- Test 1: Sending text message ---")
            try await connection.send("Hello, WebSocket!")
            print("✅ Text message sent")

            try await Task.sleep(for: .seconds(1))

            // Test 2: Send ping (pong response will cause EINVAL)
            print("\n--- Test 2: Sending ping frame ---")
            print("Expecting EINVAL when pong is received...")

            let pingMetadata = NWProtocolWebSocket.Metadata(opcode: .ping)
            try await connection.ping(Data()) {
                pingMetadata
            }
            print("✅ Ping sent, waiting for pong...")

            // Wait for pong response
            try await Task.sleep(for: .seconds(2))

            // Cleanup
            receiveTask.cancel()

            print("\n=== Test Complete ===")
            print("If you saw 'EINVAL error occurred!' above, the bug is reproduced.")
        }
    }
}
  1. The receive() call fails with error when pong arrives:
❌ EINVAL error occurred! (POSIXErrorCode 22: Invalid argument)

Test Results

ScenarioResult
Send/receive text (opcode 1)✅ OK
Client sends ping, receives pong❌ EINVAL on pong receive

Expected Behavior

The receive() method should successfully return ping and pong frames, or at minimum, handle them internally without throwing an error. The autoReplyPing option should allow automatic pong responses without disrupting the receive loop.

Actual Behavior

When a ping or pong control frame is received:

  1. The receive() method throws NWError.posix(.EINVAL)
  2. The frame never reaches user code (no opcode check is possible)
  3. The connection remains valid, but the receive loop is interrupted

Workaround

Catch the EINVAL error and restart the receive loop:

while !Task.isCancelled {
    do {
        let received = try await connection.receive()
        // Process message
    } catch let error as NWError {
        if case .posix(let code) = error, code == .EINVAL {
            // Control frame caused EINVAL, continue receiving
            continue
        }
        throw error
    }
}

This workaround allows continued operation but:

  • Cannot distinguish between ping-related EINVAL and other EINVAL errors
  • Cannot access the ping/pong frame content
  • Cannot implement custom ping/pong handling

Impact

  • WebSocket connections to servers that send periodic pings will experience repeated EINVAL errors
  • Applications must implement workarounds that may mask other legitimate errors

Additional Information

  • Packet capture confirms ping/pong frames are correctly transmitted at the network level
  • The error occurs in the Network framework's internal processing, before reaching user code
Answered by DTS Engineer in 867956022

This is only with the new NetworkConnection API, right? So things work if you use the older NWConnection?

If so, my advice is that you file a bug against the new API.

Please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

This is only with the new NetworkConnection API, right? So things work if you use the older NWConnection?

If so, my advice is that you file a bug against the new API.

Please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

NetworkConnection throws EINVAL when receiving ping/pong control frames
 
 
Q