diff --git a/SignalServiceKit/src/Network/WebSocketPromise.swift b/SignalServiceKit/src/Network/WebSocketPromise.swift index 10b92bb1ca..f4d85e0933 100644 --- a/SignalServiceKit/src/Network/WebSocketPromise.swift +++ b/SignalServiceKit/src/Network/WebSocketPromise.swift @@ -4,6 +4,7 @@ // import Foundation +import Network extension WebSocketFactory { func webSocketPromise(request: URLRequest, queue: DispatchQueue) -> WebSocketPromise? { @@ -149,6 +150,14 @@ final class WebSocketPromise: SSKWebSocketDelegate { case .waitingForAllResponses(let future): switch state.socketError { case .some(WebSocketError.closeError(statusCode: WebSocketError.normalClosure, closeReason: _)): + // On iOS 13, we expect exactly one message. If we notice "!= 1" results on + // well-behaved platforms, surface it as an error since it's probably + // leading to incorrect behavior on iOS 13. + owsAssertDebug(state.receivedMessages.count == 1, "Must be exactly one message.") + let receivedMessages = state.receivedMessages + state.receivedMessages = [] + return { future.resolve(receivedMessages) } + case .some(let socketError) where isNormalOS13Closure(state, error: socketError): let receivedMessages = state.receivedMessages state.receivedMessages = [] return { future.resolve(receivedMessages) } @@ -162,6 +171,36 @@ final class WebSocketPromise: SSKWebSocketDelegate { } } + /// Checks if this is a "normalClosure" on iOS 13. + /// + /// On iOS 13, we get back a "Socket is not connected" error instead of a + /// callback to `urlSession(_:webSocketTask:didCloseWith:reason:)` when the + /// web socket is closed normally (ie, with a closeCode of 1000). However, + /// if the web socket is closed *abnormally*, the delegate method is called. + /// As a result, we interpret "Socket is not connected" as a normal teardown + /// on iOS 13 in some scenarios. + /// + /// Importantly, this heuristic behaves correctly since we expect exactly + /// one response when calling waitForAllResponses(). If we have a response, + /// it's successful. If we don't, it's a failure. In the future, if we + /// expect an arbitrary number of responses, we'll need a different + /// heuristic (it becomes impossible to know if we're still waiting for + /// additional responses or if we've received them all -- that EOF behavior + /// is exactly what the closeCode provides in the general case). + private func isNormalOS13Closure(_ state: State, error: Error) -> Bool { + guard #unavailable(iOS 14) else { + return false + } + let nsError = error as NSError + guard nsError.domain == kNWErrorDomainPOSIX as String && nsError.code == ENOTCONN else { + return false + } + guard state.receivedMessages.count == 1 else { + return false + } + return true + } + // MARK: - SSKWebSocketDelegate func websocketDidConnect(socket: SSKWebSocket) {