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 onError — onError 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.