Close socket in tapir-zio-http-server

Hello.

I am trying to consume a Tapir websocket using tapir-zio-http-server:

val joinRoom: PublicEndpoint[
  (RoomId, PlayerName),
  RoomError,
  Stream[Throwable, ClientMessage] => Stream[Throwable, ServerMessage],
  ZioStreams & WebSockets
] =
  root
    .in(path[RoomId])
    .in(jsonBody[PlayerName])
    .get
    .out(webSocketBody[ClientMessage, CodecFormat.Json, ServerMessage, CodecFormat.Json](ZioStreams))
    .errorOut(jsonBody[RoomError])
private val routes = ZioHttpInterpreter().toHttp(List(
  Endpoints.joinRoom.zServerLogic(joinRoom)
))

def joinRoom(id: RoomId, player: PlayerName): IO[RoomError, Stream[Throwable, ClientMessage] => Stream[Throwable, ServerMessage]] = ...

Everything work well but I find no documentation about closing the websocket. In ZIO-HTTP, Channel#shutdown (note it’s different from ZIO’s ZChannel) is used to close a socket but as far as I know I only have access to a zio.stream.Stream[Throwable, ClientMessage].

How can I close the websocket when a specific message is received?

The documentation doesn’t mention it, but I found that if your output stream is Stream[Throwable, Option[ServerMessage]] then when you return a None in the stream it will close the websocket from the server end. As long as you keep returning Some(message) then the socket will stay open.

Similarly, if the input stream has type Stream[Throwable, Option[ClientMessage]] then your controller logic will receive a None if the websocket is closed from the client end.

1 Like

Yes! That’s due to codecs, which map Nones to close frames: tapir/core/src/main/scala/sttp/tapir/Codec.scala at master · softwaremill/tapir · GitHub

I also updated the docs: WS close docs · softwaremill/tapir@85fe7a4 · GitHub

2 Likes

Thank you for the insights! The linked code was really helpful to make my own Codec.

object ServerMessage:
  given toWSFrameCodec: Codec[WebSocketFrame, ServerMessage, CodecFormat.Json] =
    val frameJsonCodec = Codec.textWebSocketFrame(using zioCodec[ServerMessage])
    
    Codec.fromDecodeAndMeta[WebSocketFrame, ServerMessage, CodecFormat.Json](CodecFormat.Json()) {
      case WebSocketFrame.Close(code, reason) => DecodeResult.Value(CloseRoom(code, reason))
      case frame => frameJsonCodec.decode(frame)
    } {
      case CloseRoom(code, reason) => WebSocketFrame.Close(code, reason)
      case msg => frameJsonCodec.encode(msg)
    }