WebSocket extensions API

Ktor WebSocket API supports writing your own extensions(such as RFC-7692) or any custom extensions.

The API is production ready, but may be slightly modified in a minor release. This is why it’s marked with the @ExperimentalWebSocketExtensionsApi annotation. If you want to use this API you need to OptIn:

  1. @OptIn(ExperimentalWebSocketExtensionsApi::class)

If you want to leave your feedback or subscribe on updates, check KTOR-688 design issue.

Installing extension

To install and configure the extensions we provide two methods: extensions and install which can be used in the following way:

  1. install(WebSockets) {
  2. extensions { /* WebSocketExtensionConfig.() -> Unit */
  3. install(MyWebSocketExtension) { /* MyWebSocketExtensionConfig.() -> Unit */
  4. /* Optional extension configuration. */
  5. }
  6. }
  7. }

The extensions are used in order of installation.

Checking if the extension is negotiated

All installed extensions go through the negotiation process, and those that are successfully negotiated are used during the request. You can use WebSocketSession.extensions: : List<WebSocketExtension<*>> property with list of all extensions used by for the current session.

There are two methods to check if the extension is in use: WebSocketSession.extension and WebSocketSession.extensionOrNull:

  1. webSocket("/echo") {
  2. val myExtension = extension(MyWebSocketException) // will throw if `MyWebSocketException` is not negotiated
  3. // or
  4. val myExtension = extensionOrNull(MyWebSocketException) ?: close() // will close the session if `MyWebSocketException` is not negotiated
  5. }

Writing a new extension

There are two interfaces for implementing a new extension: WebSocketExtension<ConfigType: Any> and WebSocketExtensionFactory<ConfigType : Any, ExtensionType : WebSocketExtension<ConfigType>>. A single implementation can work for both clients and servers.

Below is an example of how a simple frame logging extension can be implemented:

  1. class FrameLoggerExtension(val logger: Logger) : WebSocketExtension<FrameLogger.Config> {

The feature has two groups of fields and methods. The first group is for extension negotiation:

  1. /** List of protocols will be send in client request for negotiation **/
  2. override val protocols: List<WebSocketExtensionHeader> = emptyList()
  3. /**
  4. * This method will be called for server and will process `requestedProtocols` from client.
  5. * In the result it will return list of extensions that server agrees to use.
  6. */
  7. override fun serverNegotiation(requestedProtocols: List<WebSocketExtensionHeader>): List<WebSocketExtensionHeader> {
  8. logger.log("Server negotiation")
  9. return emptyList()
  10. }
  11. /**
  12. * This method will be called on the client with list of protocols, produced by `serverNegotiation`. It will decide if this extensions should be used.
  13. */
  14. override fun clientNegotiation(negotiatedProtocols: List<WebSocketExtensionHeader>): Boolean {
  15. logger.log("Client negotiation")
  16. return true
  17. }

The second group is the place for actual frame processing. Methods will take a frame and produce a new processed frame if necessary:

  1. override fun processOutgoingFrame(frame: Frame): Frame {
  2. logger.log("Process outgoing frame: $frame")
  3. return frame
  4. }
  5. override fun processIncomingFrame(frame: Frame): Frame {
  6. logger.log("Process incoming frame: $frame")
  7. return frame
  8. }

There are also some implementation details: the feature has Config and reference to the origin factory.

  1. class Config {
  2. lateinit var logger: Logger
  3. }
  4. /**
  5. * Factory which can create current extension instance.
  6. */
  7. override val factory: WebSocketExtensionFactory<Config, FrameLogger> = FrameLoggerExtension

The factory is usually implemented in a companion object (similar to regular features):

  1. companion object : WebSocketExtensionFactory<Config, FrameLogger> {
  2. /* Key to discover installed extension instance */
  3. override val key: AttributeKey<FrameLogger> = AttributeKey("frame-logger")
  4. /** List of occupied rsv bits.
  5. * If the extension occupy a bit, it can't be used in other installed extensions. We use that bits to prevent feature conflicts(prevent to install multiple compression features). If you implementing feature using some RFC, rsv occupied bits should be referenced there.
  6. */
  7. override val rsv1: Boolean = false
  8. override val rsv2: Boolean = false
  9. override val rsv3: Boolean = false
  10. /** Create feature instance. Will be called for each WebSocket session **/
  11. override fun install(config: Config.() -> Unit): FrameLogger {
  12. return FrameLogger(Config().apply(config).logger)
  13. }
  14. }
  15. }