Skip to content

OkHttp — WebSocket Logging

WebSocket List

WebSocket Detail

Setup

Use the wiretapped() extension on any WebSocketListener:

val request = Request.Builder().url("wss://echo.websocket.org").build()
client.newWebSocket(request, myListener.wiretapped())

Full example

val myListener = object : WebSocketListener() {
    override fun onOpen(webSocket: WebSocket, response: Response) {
        webSocket.send("Hello!")  // Automatically logged (outgoing)
    }

    override fun onMessage(webSocket: WebSocket, text: String) {
        println("Received: $text")  // Automatically logged (incoming)
    }

    override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
        webSocket.close(1000, null)
    }

    override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
        println("Closed: $code $reason")
    }

    override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
        println("Failed: ${t.message}")
    }
}

val request = Request.Builder().url("wss://echo.websocket.org").build()
client.newWebSocket(request, myListener.wiretapped())

How It Works

WiretapOkHttpWebSocketListener wraps all WebSocketListener callbacks:

Callback What's Logged
onOpen Connection opened (status: Open). WebSocket is wrapped for outgoing logging
onMessage(text) Text message received
onMessage(bytes) Binary message received (auto-decoded as text when it looks like UTF-8, otherwise [Binary: N bytes])
onClosing Status updated to Closing with close code/reason
onClosed Status updated to Closed with timestamp
onFailure Status updated to Failed with error message

All events are delegated to your original listener after logging.

Outgoing Message Logging

The webSocket parameter passed to your onOpen() callback is actually a WiretapWebSocket that intercepts send() calls:

override fun onOpen(webSocket: WebSocket, response: Response) {
    // webSocket is a WiretapWebSocket — send() calls are logged automatically
    webSocket.send("Hello!")        // Logged: Text, Sent, "Hello!"
    webSocket.send(byteString)      // Logged: Binary, Sent, auto-decoded text or "[Binary: N bytes]"
}

All other WebSocket methods (close(), cancel(), request(), queueSize()) pass through to the original.

What Gets Logged

Connection

  • URL
  • Request headers
  • Protocol
  • Status transitions (Open → Closing → Closed / Failed)
  • Close code and reason
  • Failure message

Messages

  • Direction (Sent / Received)
  • Content type (Text / Binary)
  • Content (text, decoded binary, or [Binary: N bytes])
  • Byte count
  • Timestamp

Configuration

Use the config DSL overload to configure WebSocket logging:

client.newWebSocket(request, myListener.wiretapped {
    enabled = BuildConfig.DEBUG

    // How to render Binary frames. Defaults to Auto.
    binaryDecoding = BinaryFrameDecoding.Auto
    // = BinaryFrameDecoding.Utf8         // always decode as UTF-8 (replacement chars on invalid bytes)
    // = BinaryFrameDecoding.Placeholder  // never decode, always show "[Binary: N bytes]"
    // = BinaryFrameDecoding.Custom { bytes -> bytes.toHexPreview() }
})

Or use the simple enabled parameter:

client.newWebSocket(request, myListener.wiretapped(enabled = false))

binaryDecoding

Auto (default) tries strict UTF-8 — if the payload is valid printable text (tab/LF/CR plus the SignalR Core record separator 0x1E are tolerated, matching what the JSON hub protocol can carry as raw bytes) it's shown as text, otherwise it falls back to [Binary: N bytes]. This lets libraries that ship text-over-binary (e.g. SignalRKore) appear readable without misrepresenting genuine binary like protobuf or MessagePack.

Custom lets you supply your own renderer for non-UTF-8 charsets, hex previews, or pretty-printing.

No-op

The noop module (wiretap-okhttp-noop) provides the same wiretapped() extensions but delegates all callbacks directly without logging.