Websockets. But typesafe. And testable. Without the Server.

december 2017 / @daviddenton

Reaction to the last post introducing http4k was pretty good, and one of the most popular questions was: "But what about Websockets"?

The answer to that question at the time was an emphatic "Not yet" - because they didn't fit the "Server as a Function" model, and the team hadn't worked out a way to deliver them in a simple, offline testable* way.

Well, a month is a long time, and we've been beavering away, so now we're thrilled to release Websockets for http4k, which are:

Simple : using the same style of API as the rest of http4k, allowing the same dynamic path-based routing as is available for standard HttpHandlers .

: using the same style of API as the rest of http4k, allowing the same dynamic path-based routing as is available for standard . Typesafe : Marshall and unmarshall typed objects from Websocket Messages using the established Lens API.

: Marshall and unmarshall typed objects from Websocket Messages using the established Lens API. Testable: This is something that is massively important to us - and just like standard HttpHandlers, http4k Websockets are completely testable in a synchronous online or offline environment. No. Server. Required.

Details schmeetails...

Just as with HttpHandlers, the here are 2 basic function types which make up the core of the Websocket routing API:

A WsHandler - represented as a typealias: (Request) -> WsConsumer? . This is responsible for matching an incoming HTTP upgrade request to a websocket.

- represented as a typealias: . This is responsible for matching an incoming HTTP upgrade request to a websocket. WsConsumer - represented as a typealias: (WebSocket) -> Unit . This function is called on connection and allow the API user to react to events coming from the connected Websocket by attaching listeners.

Additionally, WsMessage objects are used for actual communication - ie. a message which is sent or received on a Websocket. This message can take advantage of the typesafety accorded to other entities in http4k by using the Lens API. And just like the http4k HTTP message model, WsMessages are immutable data classes.

An example server

The example below shows how:

Websockets can be dynamically routed

Lens-based marshalling of Websocket message objects using Jackson.

WsHandler can be combined with an HttpHandler to make a PolyHandler - an application which can serve many protocols. Conversion of the PolyHandler to a supporting Server can be done via the standard asServer() mechanism, or it can be kept offline for ultra-fast in-memory testing:

package blog.typesafe_websockets import org.http4k.core.HttpHandler import org.http4k.core.Request import org.http4k.core.Response import org.http4k.core.Status.Companion.OK import org.http4k.format.Jackson.auto import org.http4k.lens.Path import org.http4k.routing.bind import org.http4k.routing.websockets import org.http4k.server.Jetty import org.http4k.server.asServer import org.http4k.websocket.PolyHandler import org.http4k.websocket.Websocket import org.http4k.websocket.WsHandler import org.http4k.websocket.WsMessage // in json, this looks like: {"value": 123, "currency: "EUR" } data class Money ( val value : Int , val currency : String ) fun main () { // we use the Lens API to convert between the WsMessage and the Money instance, and to // dynamically bind the "name" from the path val moneyLens = WsMessage . auto < Money >(). toLens () val nameLens = Path . of ( "name" ) // the routing API is virtually identical to the standard http4k http routing API. // on connection, the bound WsConsumer is called with the Websocket instance val ws : WsHandler = websockets ( "/hello" bind websockets ( "/{name}" bind { ws : Websocket -> val name = nameLens ( ws . upgradeRequest ) ws . onMessage { val received = moneyLens ( it ) ws . send ( moneyLens ( received )) } ws . onClose { println ( "closed" ) } ws . send ( WsMessage ( "hello $name" )) } ) ) val http : HttpHandler = { _ : Request -> Response ( OK ). body ( "hiya world" ) } // the poly-handler can serve both http and ws protocols. PolyHandler ( http , ws ). asServer ( Jetty ( 9000 )). start (). block () }

Alternatively, you can check out the Websocket enabled http4k demo: IRC clone in 30 lines of Kotlin.

Testability

As well as API simplicity, the http4k team are very passionate about testing, and it was very important that this could be done in an out-of-container fashion - ie. in memory and with no server being started. As such, it is possible to call testWsClient() on an WsHandler to provide a synchronous API for testing. Messages and other events can be "sent" to a connected websocket and responses will be received back in a completely predictable way from the application under test.

In the below example, we have gone one step further - defining a contract test case and then providing 2 implementations of it - one for unit-testing (in memory), one using a server. http4k provides clients with an identical interface for both cases, meaning it's possible reuse the same test logic:

package blog.typesafe_websockets import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo import org.http4k.client.WebsocketClient import org.http4k.core.Method.GET import org.http4k.core.Request import org.http4k.core.Uri import org.http4k.lens.Path import org.http4k.routing.bind import org.http4k.routing.websockets import org.http4k.server.Jetty import org.http4k.server.asServer import org.http4k.testing.testWsClient import org.http4k.websocket.Websocket import org.http4k.websocket.WsClient import org.http4k.websocket.WsHandler import org.http4k.websocket.WsMessage import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test val namePath = Path . of ( "name" ) // here is our websocket app - it uses dynamic path binding and lenses val testApp : WsHandler = websockets ( "/{name}" bind { ws : Websocket -> val name = namePath ( ws . upgradeRequest ) ws . send ( WsMessage ( "hello $name" )) } ) // this is the abstract contract that defines the behaviour to be tested abstract class WebsocketContract { // subclasses only have to supply a blocking WsClient abstract fun client (): WsClient @Test fun `echoes back connected name` () { assertThat ( client (). received (). take ( 1 ). toList (), equalTo ( listOf ( WsMessage ( "hello bob" )))) } } // a unit test version of the contract - it connects to the websocket in memory with no network class WebsocketUnitTest : WebsocketContract () { override fun client () = cookbook . websockets . testApp . testWsClient ( Request ( GET , "/bob" )) !! } // a integration test version of the contract - it starts a server and connects to the websocket over the network class WebsocketServerTest : WebsocketContract () { override fun client () = WebsocketClient . blocking ( Uri . of ( "ws://localhost:8000/bob" )) private val server = cookbook . websockets . testApp . asServer ( Jetty ( 8000 )) @BeforeEach fun before () { server . start () } @AfterEach fun after () { server . stop () } }

Fin

Websocket support is now available for the Jetty server backend in http4k v3.2.0 . We plan to roll out support for other server-backends in due course. Have a play a let us know what you think...