Table of contents
In this article I'm going to discuss how you can extend the functionality of Facebook's excellent Stetho library to debug WebSockets connections on Android. While Stetho allows you to inspect HTTP(S) requests, SQLite databases and Shared Preferences values out of the box, it does omit WebSockets. We are going to write a simple utility class that fixes this.
Brief Stetho overview
Stetho is a library developed by Facebook that allows an android app (running on an emulator or on a device connected via adb
to a laptop) to be debugged using the excellent Chrome Debug Tools as if it were a website. You can monitor the HTTP requests your app does, view and modify SQLite databases, inspect the view hierarchy, or view and change shared preferences on the fly. You can check out the library here.
The problem
Chrome Debug Tools do support WebSockets, however the folks at Facebook decided to implement support for it in Stetho only halfway. Looking at the code, there seems to be an object called NetworkEventReporter
which has methods for reporting events such as webSocketFrameReceived
or webSocketFrameSent
, but there seems to be no utility classes to call these at the appropriate time for any WebSockets library, not even the popular OkHTTP3.
While working on my pet project OnlineGo I often struggled with debugging the fairly complex protocol that is built by the fine folks at OGS on top of Socket.IO (which is in itself a protocol built on top of WebSockets). Sure, you can log the communication on the console, but logcat has its limitations. I thus decided to spend some time making Stetho play nice with OkHttp.
The solution
Looking at the NetworkEventReporter
we can identify the following events that we need to intercept and forward to the reporter:
webSocketCreated
webSocketClosed
webSocketFrameError
webSocketFrameReceived
(binary and text)webSocketFrameSent
(binary and text)
Luckily, OkHTTP 3 has a nice WebSocket.Factory
interface that you can simply plug in. This interface is implemented by OkHttpClient
, but we can override it with our own implementation. It has a single method that gets called by the library when a new websocket connection needs to be initialized:
fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket
The plan is then to create a new WebSocket.Factory
class that has a plain OkHttpClient
member to which it delegates the newWebSocket
calls. This new class also wraps the returned WebSocket
and the provided WebSocketListener
into new objects that notify the NetworkEventReporter
when the relevant events occour. Here is the code:
class StethoWebSocketsFactory(private val httpClient: OkHttpClient) : WebSocket.Factory {
private val reporter = NetworkEventReporterImpl.get()
override fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket {
val requestId = reporter.nextRequestId()
val newListener = StethoWebSocketListener(listener, requestId)
val wrappedSocket = httpClient.newWebSocket(request, newListener)
return StethoWebSocket(wrappedSocket, requestId)
}
inner class StethoWebSocketListener(
private val listener: WebSocketListener,
private val requestId: String
) : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
listener.onOpen(webSocket, response)
reporter.webSocketCreated(requestId, webSocket.request().url().toString())
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
listener.onClosed(webSocket, code, reason)
reporter.webSocketClosed(requestId)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
listener.onFailure(webSocket, t, response)
reporter.webSocketFrameError(requestId, t.message)
}
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
listener.onMessage(webSocket, bytes)
reporter.webSocketFrameReceived(
SimpleBinaryInspectorWebSocketFrame(requestId, bytes.toByteArray())
)
}
override fun onMessage(webSocket: WebSocket, text: String) {
listener.onMessage(webSocket, text)
reporter.webSocketFrameReceived(
SimpleTextInspectorWebSocketFrame(requestId, text)
)
}
}
inner class StethoWebSocket(
private val wrappedSocket: WebSocket,
private val requestId: String
) : WebSocket {
private val reporter = NetworkEventReporterImpl.get()
override fun queueSize() = wrappedSocket.queueSize()
override fun send(text: String): Boolean {
reporter.webSocketFrameSent(SimpleTextInspectorWebSocketFrame(requestId, text))
return wrappedSocket.send(text)
}
override fun send(bytes: ByteString): Boolean {
reporter.webSocketFrameSent(SimpleBinaryInspectorWebSocketFrame(requestId, bytes.toByteArray()))
return wrappedSocket.send(bytes)
}
override fun close(code: Int, reason: String?) = wrappedSocket.close(code, reason)
override fun cancel() = wrappedSocket.cancel()
override fun request() = wrappedSocket.request()
}
}
If you are using the raw WebSockets
directly, then you can simply use the factory as is instead of your OkHttpClient
, however for Socket.IO
the following code is needed:
socket = IO.socket("https://online-go.com", IO.Options().apply {
transports = arrayOf("websocket")
if(BuildConfig.DEBUG) {
webSocketFactory = StethoWebSocketsFactory(httpClient)
}
})
And that's it really, you can now run the app, fire up Stetho and look under the Network / WebSockets tab and enjoy this:
You can see the class at work in the OnlineGo app repo.
Conclusion
Using the drop-in utility class described above you can easily enable Stetho debugging for your WebSockets based Android application. Happy debugging!