apple / swift-nio

Event-driven network application framework for high performance protocol servers & clients, non-blocking.
https://swiftpackageindex.com/apple/swift-nio/documentation
Apache License 2.0
7.98k stars 650 forks source link

NIOConnectionErrors #2577

Closed mcantillon21 closed 11 months ago

mcantillon21 commented 1 year ago

Expected behavior

Everything works: connection to server, sending the request, receiving the responses' audio stream and playing it outloud in real-time

Actual behavior

Failed to connect to the server: NIOConnectionError(host: "nox-devices--nox-memory-fastapi-app.modal.run", port: 443, dnsAError: Optional(NIOCore.SocketAddressError.unknown(host: "nox-devices--nox-memory-fastapi-app.modal.run", port: 443)), dnsAAAAError: Optional(NIOCore.SocketAddressError.unknown(host: "nox-devices--nox-memory-fastapi-app.modal.run", port: 443)), connectionErrors:

Steps to reproduce

For some reason this works on my Watch Simulator but not my physical watch which is arguably more important.

Not sure why this is happening, but my guess is it has something to do with the TLS, SSL configuration. It doesn't like moving to a different network, but the watch has a cellular plan so it should use that.

This is probably the most relevant function here:

` func sendTranscriptionToQueryEndpoint(transcription: String) { print("Sending transcription to query endpoint...") let userID = "254439ca-8ea0-4430-83e5-9ef1f885d7fc"

    let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
    let tlsConfiguration = TLSConfiguration.forClient()
    let sslContext = try! NIOSSLContext(configuration: tlsConfiguration)
    let tlsHandler = try! NIOSSLClientHandler(context: sslContext, serverHostname: "nox-devices--nox-memory-fastapi-app.modal.run")

    let bootstrap = ClientBootstrap(group: eventLoopGroup)
        .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
        .channelInitializer { channel in
            channel.pipeline.addHandler(tlsHandler).flatMap {
                channel.pipeline.addHTTPClientHandlers()
            }.flatMap {
                channel.pipeline.addHandler(HTTPHandler())
            }
        }

    var request = HTTPRequestHead(version: .init(major: 1, minor: 1), method: .POST, uri: "/query")
    request.headers.add(name: "Content-Type", value: "application/json")
    request.headers.add(name: "Host", value: "nox-devices--nox-memory-fastapi-app.modal.run")

    let body: [String: Any] = ["userID": userID, "transcript": "helloooooooo! ", "history": []]
    do {
        let bodyData = try JSONSerialization.data(withJSONObject: body)
        var requestBuffer = ByteBufferAllocator().buffer(capacity: bodyData.count)
        requestBuffer.writeBytes(bodyData)
        request.headers.add(name: "Content-Length", value: String(bodyData.count))

        let channelFuture = bootstrap.connect(host: "nox-devices--nox-memory-fastapi-app.modal.run", port: 443)
        channelFuture.whenSuccess { channel in
            print("Successfully connected to the server.")

            let writeFuture = channel.writeAndFlush(HTTPClientRequestPart.head(request))
            writeFuture.whenSuccess {
                print("Successfully wrote request head.")
                let bodyMapped = channel.writeAndFlush(HTTPClientRequestPart.body(.byteBuffer(requestBuffer)))
                bodyMapped.whenSuccess {
                    print("Successfully wrote and flushed request body.")
                }
                bodyMapped.whenFailure { error in
                    print("Failed to write and flush request body: \(error)")
                }
            }
            writeFuture.whenFailure { error in
                print("Failed to write request head: \(error)")
            }
        }
        channelFuture.whenFailure { error in
            print("Failed to connect to the server: \(error)")
        }
    } catch {
        print("Failed to create body JSON data: \(error)")
    }
}`

`class HTTPHandler: ChannelInboundHandler { typealias InboundIn = HTTPClientResponsePart

private let audioEngine = AVAudioEngine()
private let audioPlayerNode = AVAudioPlayerNode()
private let processingQueue = DispatchQueue(label: "com.yourApp.audioProcessing")

init() {
    audioEngine.attach(audioPlayerNode)
    audioEngine.connect(audioPlayerNode, to: audioEngine.outputNode, format: nil)
    do {
        try audioEngine.start()
    } catch {
        print("Error starting audio engine: \(error)")
    }
}

func channelRead(context: ChannelHandlerContext, data: NIOAny) {
    let httpResponsePart = unwrapInboundIn(data)
    switch httpResponsePart {
    case .head(let responseHeader):
        print("Received response: \(responseHeader)")
    case .body(let byteBuffer):
        // Here you can handle the received data chunk
        if let data = byteBuffer.getBytes(at: 0, length: byteBuffer.readableBytes) {
            print("Received data: \(data)")
            playAudioData(data)
        }
    case .end(let headers):
        print("Response ended with headers: \(String(describing: headers))")
        audioEngine.stop()
    }
}

private func playAudioData(_ data: [UInt8]) {
    guard let audioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 22050, channels: 1, interleaved: false) else {
        print("Failed to create audio format")
        return
    }

    guard let audioBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: UInt32(data.count) / 2) else {
        print("Failed to create audio buffer")
        return
    }
    audioBuffer.frameLength = UInt32(data.count) / 2

    guard let audioBufferPointer = audioBuffer.floatChannelData?[0] else {
        print("Failed to obtain audio buffer pointer")
        return
    }

    data.withUnsafeBytes {
        guard let baseAddress = $0.bindMemory(to: Float32.self).baseAddress else {
            print("Failed to bind memory")
            return
        }
        audioBufferPointer.assign(from: baseAddress, count: data.count / 2)
    }

    let audioEngine = AVAudioEngine()
    let playerNode = AVAudioPlayerNode()
    audioEngine.attach(playerNode)
    audioEngine.connect(playerNode, to: audioEngine.mainMixerNode, format: audioFormat)

    playerNode.scheduleBuffer(audioBuffer, completionHandler: nil)

    do {
        try audioEngine.start()
    } catch {
        print("Failed to start audio engine: \(error.localizedDescription)")
        return
    }

    playerNode.play()
}

} ` Happy to provide more context, but I have been struggling with this for the past week. Would love any help.

SwiftNIO version/commit hash

Latest

Lukasa commented 1 year ago

The error you're seeing is a DNS resolution failure: we're failing to resolve nox-devices--nox-memory-fastapi-app.modal.run as a hostname.

Is that hostname behind a VPN or some other limiter? Additionally, does your app have appropriate watch entitlements for networking?

mcantillon21 commented 1 year ago

I don't think it's behind any VPN. It worked well with my android watch.

Once interesting thing is that when I run nslookup theres some multiple IPs, maybe for load balancing.

(base) mollycantillon@DN0a1f6d8d ~ % nslookup nox-devices--nox-memory-fastapi-app.modal.run Server: 171.67.1.234 Address: 171.67.1.234#53

Non-authoritative answer: Name: nox-devices--nox-memory-fastapi-app.modal.run Address: 3.217.137.171 Name: nox-devices--nox-memory-fastapi-app.modal.run Address: 3.222.214.40 Name: nox-devices--nox-memory-fastapi-app.modal.run Address: 44.217.9.182 Name: nox-devices--nox-memory-fastapi-app.modal.run Address: 44.217.60.1 Name: nox-devices--nox-memory-fastapi-app.modal.run Address: 54.156.152.125 Name: nox-devices--nox-memory-fastapi-app.modal.run Address: 3.211.143.0

Also not sure what watch entitlements I would need for networking, I have enabled background modes so far

What is the easiest workaround here? Just looking for the quickest solution.

Lukasa commented 1 year ago

There is a tech-note that discusses watchOS networking. The code you have here is not going to work on watchOS for the following reasons:

  1. You're using NIO POSIX, which uses BSD sockets. BSD sockets don't work on the watch.
  2. You're doing "low-level networking", which requires that your app meet one of three requirements:
    1. Be an audio-streaming app and have an active audio streaming session open.
    2. Be a VoIP app with an active CallKit session.
    3. Have a tvOS-compatible service listener.

Assuming you meet one of the latter three criteria (it seems like you have an audio streaming session so that may well work), you just need to swap your backend. You can use swift-nio-transport-services instead of NIO POSIX. It should require only a small tweak: replace the NIOPOSIX import with NIOTransportServices, and then use NIOTSConnectionBootstrap and NIOTSEventLoopGroup instead of ClientBootstrap and MultiThreadedEventLoopGroup. The rest should all work.

mcantillon21 commented 1 year ago

Thanks I just made the switch. However, I'm still getting the following error no matter how long I get the timeout to be. Where can I declare that my app meets the first requirement?

Failed to connect to the server: connectTimeout(NIOCore.TimeAmount(nanoseconds: 10000000000))

Lukasa commented 1 year ago

You don't declare it, you just need an active audio session before you call connect.

Are you using NIOTransportServices?

mcantillon21 commented 1 year ago

Yes I am but same error. What else am I supposed to change here?

It just fails to connect to the server. In the bootstrap.connect line.

`
func sendTranscriptionToQueryEndpoint(transcription: String) { print("Sending transcription to query endpoint...") let userID = "254439ca-8ea0-4430-83e5-9ef1f885d7fc"

    let group = NIOTSEventLoopGroup()
    let bootstrap = NIOTSConnectionBootstrap(group: group)
        .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
        .channelInitializer { channel in
            channel.pipeline.addHTTPClientHandlers()
        }

    var request = HTTPRequestHead(version: .init(major: 1, minor: 1), method: .POST, uri: "/query")
    request.headers.add(name: "Content-Type", value: "application/json")

    let body: [String: Any] = ["userID": userID, "transcript": transcription, "history": []]
    do {
        let bodyData = try JSONSerialization.data(withJSONObject: body)
        var requestBuffer = ByteBufferAllocator().buffer(capacity: bodyData.count)
        requestBuffer.writeBytes(bodyData)
        request.headers.add(name: "Content-Length", value: String(bodyData.count))

        let channelFuture = bootstrap.connect(host: "nox-devices--nox-memory-fastapi-app.modal.run", port: 443)
        channelFuture.whenSuccess { channel in
            print("Successfully connected to the server.")

            let writeFuture = channel.writeAndFlush(HTTPClientRequestPart.head(request))
            writeFuture.whenSuccess {
                print("Successfully wrote request head.")
                let bodyMapped = channel.writeAndFlush(HTTPClientRequestPart.body(.byteBuffer(requestBuffer)))
                bodyMapped.whenSuccess {
                    print("Successfully wrote and flushed request body.")
                }
                bodyMapped.whenFailure { error in
                    print("Failed to write and flush request body: \(error)")
                }
            }
            writeFuture.whenFailure { error in
                print("Failed to write request head: \(error)")
            }
        }
        channelFuture.whenFailure { error in
            print("Failed to connect to the server: \(error)")
        }
    } catch {
        print("Failed to create body JSON data: \(error)")
    }
}`
PrLion commented 1 year ago

Hello, I faced with connection error too.

For example I can connect to host with .pem certificate an .key from USA. All users which tried to connect from USA it works. Users from India can't connect. We tried to check host through telnet and mosquito it works. but not from our app.

Do you know where we can have issue?