#Unable to connect to a websocket

1 messages · Page 1 of 1 (latest)

livid elk
#

Hi,
I'm trying to connect to a websockets but somehow it doesn't work(no errors, no response). Did I misunderstand something in the docs?
What I'm trying to do is to use my backend as a bridge between my app and a third party api & websockets.

Note: I'm just starting to learn Vapor.

    app.webSocket("echo") { request, ws async in
        do {
            try await WebSocket.connect(to: "wss://ws.coincap.io/prices?assets=bitcoin", on: app.eventLoopGroup.next()) { innerWS in
                // connection successful, innerWS.isClosed returns FALSE here

                innerWS.onText { _, string async in
                    // this whole block is not being called
                }
                
                innerWS.onBinary { _, byte async in
                    // this whole block is not being called
                }
            }
        } catch {
            // no errors here
        }
    }
grim hearth
#

@livid elk WebSocket.connect returns a wbsocket instance that you need to keep alive, it's most likely deinit-ed

#

The callback you see there is called on or before connect (don't remember which). So isClosed should indeed be false

#

Also, make sure you use the websocket reference within onText and onBinary to prevent a retain cycle

livid elk
#

Thank you @grim hearth but WebSocket.connect doesn't seem to return anything, it just throws. Or did you mean that we should keep reference to the WebSocket returned from the onUpgrade callback?

night hawkBOT
grim hearth
#

Hrm, I'm mistaking this with a different API call I guess

#

But yes, you'll need to keep it alive

#

onText and onBinary are not means to keep it alive, so the current WebSocketKit API is a bit awkward for this use case. It does work though, just a bit confusing

livid elk
#

Still can't seem to make it work. How exactly do I keep it alive? Maybe I'm misunderstanding something. Here is what I currently have.

class WebSocketStore {
    private(set) var webSockets: [UUID: WebSocket] = [:]
    
    func add(_ ws: WebSocket) -> UUID {
        let id = UUID()
        webSockets[id] = ws
        
        print("WEBSOCKET ADDED")
        return id
    }
    
    func remove(id: UUID) {
        webSockets.removeValue(forKey: id)
        
        print("WEBSOCKET REMOVED")
    }
}

final class WebSocketController: RouteCollection {
    private let webSocketStore = WebSocketStore()
    
    func boot(routes: RoutesBuilder) throws {
        let websocket = routes.grouped("websocket")
        websocket.get(use: connect)
    }
    
    func connect(req: Request) async throws -> HTTPStatus {
        let id = UUID()
        try await WebSocket.connect(to: "wss://ws.coincap.io/prices?assets=bitcoin", on: req.eventLoop.next()) { ws in
            let id = self.webSocketStore.add(ws)
            
            req.eventLoop.scheduleRepeatedTask(initialDelay: .seconds(1), delay: .seconds(3)) { task in
                self.webSocketStore.webSockets[id]?.onText { ws, text in
                    print(text) // not called
                }
                
                // send pings to keep alive?
                self.webSocketStore.webSockets[id]?.sendPing()
                self.webSocketStore.webSockets[id]?.send("")
                // just to make sure
                ws.sendPing()
                ws.send("")
                
                print("Timer fired!") // printed every 3 seconds as expected
            }
            
            ws.onClose.whenComplete { _ in
                self.webSocketStore.remove(id: id) // not called as expected
            }
        }
        return .ok
    }
}
grim hearth
#

You'll want to set up your onText and onBinary only once, at the start, to ensure you don't miss any messages.

#

And you can just re-use the reference to ws there, instead of accessing webSocketStore.webSockets[id]

#

Although you will need to make sure the repeated task is cancelled as well, even for demo purposes

#

Sending pings to keep a connection alive makes a lot of sense, I've seen plenty of sockets get disconnected when idle. Though you'll want to do so by setting ws.pingInterval

#

What kind of messages are you expecting to receive from that server? A binary or text stream?

livid elk
#

it's text. Have tried it in my iOS app

grim hearth
#

One more tip: You can use ObjectIdentifier to get a unique ID for your class' instance

#

Then you don't need to generate and track one

#

I would definitely say you're retaining it now. So if onText isn't being called I'd have to conclude the websocket is either closed or not receiving data.

livid elk
#

Yeah, I'm still not getting any data for some reason

grim hearth
#

Do you need to send messages to their websocket API in order to receive data?

#

Like a request or handshake

livid elk
#

Nah

grim hearth
#

Or do you need to authenticate?

livid elk
#

nop, nothing, just connect

#

Here is my working swift demo:

    func wsConnect() async {
        let session = URLSession(configuration: .default, delegate: nil, delegateQueue: OperationQueue())
        let url = URL(string:  "wss://ws.coincap.io/prices?assets=bitcoin")
        
        webSocket = session.webSocketTask(with: url!)
        webSocket?.resume()
        receive()
    }
    
    func receive() {
        webSocket?.receive(completionHandler: { result in
            switch result {
            case .success(let success):
                switch success {
                case .data(let data): break
                    
                case .string(let string):
                    guard string.isEmpty == false else { return }
                    guard let data = string.data(using: .utf8) else { return }
                    
                    let response = try? JSONDecoder().decode([String: String].self, from: data)
                    DispatchQueue.main.async {
                        print(response?["bitcoin"])
                    }
                @unknown default: break
                }
            case .failure(let failure):
                print(failure)
            }
            
            DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
                receive()
            }
        })
    }

Vapor just hates me for some reason 😄

grim hearth
#

The url is the same I assume

livid elk
#

yeah

grim hearth
#

On iOS you won't need a delay for receive() btw. That only artifically slows down getting your content

#

And I'd always recommend looking at adopting structured concurrency for these things

#

If available

livid elk
#

Oh, nice to know. Thanks

grim hearth
#

Did you try my above tips? Like moving the onText callback?

livid elk
#

Yeah, I moved it above the scheduleRepeatedTask , also updated .pingInterval just in case but still nothing.

#

Can it be because I'm running it from localhost?

grim hearth
#
let promise = elg.any().makePromise(of: WebSocket.self)
        let connected = WebSocket.connect(
            to: "wss://ws.coincap.io/prices?assets=bitcoin",
            on: NIOSingletons.posixEventLoopGroup.next()
        ) { ws in
            promise.succeed(ws)
            ws.onText { ws, text in
                print(text) // not called
            }
            ws.pingInterval = .seconds(5)
        }
        connected.cascadeFailure(to: promise)
        let retain = try await promise.futureResult.get()
        try await retain.onClose.get()

This works for me

#

I'm getting events

#

Note that my code does not handle a failure to connect, so it's simply a quick demo. But I do get events

#

Just updated it to fix that