Skip to content

WebSocket Client

uni includes a cross-platform WebSocket client that runs on the JVM, Scala.js, and Scala Native. It connects to ws:// / wss:// endpoints and uses the same WebSocketHandler / WebSocketContext abstraction as the server-side WebSocket support, so handler code is symmetric on both ends.

Connecting

Http.webSocketClient returns the platform's WebSocketClient. Call connect with the URL and a handler; it returns an Rx[WebSocketContext] that emits the open context once the handshake completes (or fails with the handshake error):

scala
import wvlet.uni.http.{Http, WebSocketContext, WebSocketHandler}

val handler = new WebSocketHandler:
  override def onTextMessage(ctx: WebSocketContext, message: String): Unit =
    println(s"received: ${message}")

Http.webSocketClient
  .connect("ws://localhost:8080/ws/echo", handler)
  .subscribe { ctx =>
    ctx.send("hello")     // connection is open here
  }

The handler's onOpen(ctx) fires for the same event, so you can also do the initial send there instead of in subscribe.

Handler Callbacks

The client implements the same WebSocketHandler as the server. Every callback has a no-op default, so override only what you need:

CallbackWhen it fires
onOpen(ctx)the handshake completed
onTextMessage(ctx, message)a text message arrived
onBinaryMessage(ctx, message)a binary message arrived (Array[Byte])
onClose(ctx)the connection closed; delivered exactly once
onError(ctx, e)a handler callback threw

Errors vs. close

Transport/protocol failures map to onClose, not onErroronError is reserved for exceptions thrown inside your own callbacks. A failure before the connection opens surfaces through the connect stream (as an Rx error).

Sending and Closing

The WebSocketContext controls the connection; its send/close methods are safe to call from any thread:

scala
ctx.request                       // the original HTTP upgrade Request
ctx.send("hello")                 // text frame
ctx.send(Array[Byte](1, 2, 3))    // binary frame
ctx.close()                       // normal closure (1000)
ctx.close(1001, "going away")     // close with a status code and reason

Heartbeat (ping/pong)

A client-side ping/pong heartbeat detects a dead-but-idle connection. It is off by default and enabled by passing a ping interval (in milliseconds) as the third argument to connect:

scala
// Ping an idle connection every 30s; close it if the peer stops responding.
Http.webSocketClient.connect("wss://example.com/ws", handler, pingIntervalMillis = 30000)

When enabled, an idle interval sends a Ping; if the next interval is still idle with the ping unanswered, the client closes the connection (status 1011, "ping timeout"). Any inbound frame proves liveness and clears a pending ping.

Not available on Scala.js

The browser/Node global WebSocket API cannot send protocol pings, so pingIntervalMillis is ignored on Scala.js. Use it on JVM/Native, or implement application-level keepalive messages if you need liveness detection in the browser.

Platform Support

PlatformBackendTransportNotes
JVMJavaWebSocketClientjava.net.http.WebSocket (Java 11+)ws:// and wss://; heartbeat supported
Scala.jsJSWebSocketClientglobal WebSocketbrowsers or Node.js ≥ 22; pingIntervalMillis ignored
Scala NativeNativeWebSocketClientraw POSIX socketws:// only (no TLS); host must be an IP or localhost (no DNS); max frame 1 MB

Http.webSocketClient resolves to the right backend automatically. On a platform without WebSocket support the base client throws NotImplementedError, but all three shipped platforms provide a backend.

See Also

Released under the Apache 2.0 License.