Web Sockets are a great way to stream data from a server to a client. Http4s is a fantastic http server for Scala.

The trick to serving Web Sockets with Http4s is that you need to get hold of WebSocketBuilder2 before you can make the HttpRoutes. You get the WebSocketBuilder by using withHttpWebSocketApp on the ServerBuilder your using.

Then you can serve or receive data as FS2 Streams of Stream[IO, WebSocketFrame] with all the power and flexibility they offer.

For example you to serve websocket data on /ws you could have a routes function like this.

def routes(ws: WebSocketBuilder2[IO]): HttpRoutes[IO] =
    HttpRoutes.of[IO] {
      case GET -> Root / "ws" =>
        val send: Stream[IO, WebSocketFrame] =
          Stream.awakeEvery[IO](1.second)
            .evalMap(_ => IO(WebSocketFrame.Text("ok")))
        val receive: Pipe[IO, WebSocketFrame, Unit] =
          in => in.evalMap(frameIn => IO(println("in " + frameIn.length)))

        ws.build(send, receive)
    }

Then in a JavaScript client, you can subscribe to the Web Socket like this.

const socket = new WebSocket("ws://localhost:8080/ws");

socket.onmessage = function (event) {
  console.log(`[message] Data received from server: ${event.data}`);
};

The complete example

import com.comcast.ip4s._
import cats.effect.{ExitCode, IO, IOApp, Resource}
import cats.syntax.all._
import fs2.{Pipe, Stream}
import org.http4s.HttpRoutes
import org.http4s.dsl.io._
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Server
import org.http4s.server.websocket.WebSocketBuilder2
import org.http4s.websocket.WebSocketFrame
import java.time.Instant
import scala.concurrent.duration._

object Main extends IOApp {

  def routes(ws: WebSocketBuilder2[IO]): HttpRoutes[IO] =
    HttpRoutes.of[IO] {
      case GET -> Root / "ws" =>
        val send: Stream[IO, WebSocketFrame] =
          Stream.awakeEvery[IO](1.second)
            .evalMap(_ => IO(WebSocketFrame.Text("ok")))
        val receive: Pipe[IO, WebSocketFrame, Unit] =
          in => in.evalMap(frameIn => IO(println("in " + frameIn.length)))

        ws.build(send, receive)
    }

  val serverResource: Resource[IO, Server] =
    EmberServerBuilder
      .default[IO]
      .withHost(ipv4"0.0.0.0")
      .withPort(port"8080")
      .withHttpWebSocketApp(ws => routes(ws).orNotFound)
      .build


  def run(args: List[String]): IO[ExitCode] = {
    Stream
      .resource(
        serverResource >> Resource.never
      )
      .compile
      .drain
      .as(ExitCode.Success)
  }
}