Skip to content

WASM/JS KtorRpcClient - not able manual re-establish connection after server restart #558

Description

@lostiniceland

Describe the bug
After establishing a websocket connection for KRPC using KtorRpcClient, and the restarting the server so that this connection is lost, web-targets (JS/WASM) are not able to handle Exceptions so that the connection can be manually recreated.

The following solutions to re-establish a connection works fine on Desktop target, but on Web nothing happens: no errors, no timeouts, nothing.
I've tried to set timeouts, use @Volatile and some other solutions to take the more asyncronous events in the browser into consideration; none of which worked on web.
It seems that the actual client on web never throws an exception or the transpiled code isn't handling the exception.

To Reproduce
Following code is the most simple approach that works fine on desktop.

  1. Start Backend & Frontend
  2. Use Frontend
  3. Restart Server
  4. Try another action in Frontend -> Dead
class ApiClient(
    private val baseUrl: String = "http://localhost:8080"
) {

   private val rpcClient = HttpClient(){
        installKrpc {
            serialization {
                json(Json {
                    serializersModule = SerializersModule {
                        include(ArrowModule)
                    }
                })
            }
        }
    }

    private var _rpcConnection: kotlinx.rpc.krpc.ktor.client.KtorRpcClient? = null
    private var _crudRpcService: RpcCrudService? = null
    // other RpcServices defined as well

    private suspend fun createConnection(): kotlinx.rpc.krpc.ktor.client.KtorRpcClient {
        return rpcClient.rpc {
            url {
                host = "localhost"
                port = 8080
                encodedPath = "/rpc"
            }
        }
    }


    private suspend inline fun <reified T> getOrCreateService(): T {
        val connection = _rpcConnection ?: createConnection().also { _rpcConnection = it }

        return when (T::class) {
            RpcCrudService::class -> _crudRpcService ?: connection.withService<RpcCrudService>().also { _crudRpcService = it }
            else -> error("Unknown service type: ${T::class}")
        } as T
    }


    suspend inline fun <R> withCrudService(block: suspend (RpcCrudService) -> R): R {
        return try {
            block(getOrCreateService<RpcCrudService>())
        } catch (e: Exception) {
            _rpcConnection = null
            _crudRpcService = null
            // other Rpc services reset here as well
            block(getOrCreateService<RpcCrudService>())
        }
    }
}

Expected behavior
Web-Targets work the same as Desktop, meaning the manual reconnection works. An internal reconnection mechanism would be preferable. Also a documented best-practise would be nice (server-restarts need to be taken into account nowadays)

Additional context
Kotlin: 2.3.0
Gradle: 8.14.3
OS: Linux
KMP: 1.9.3
Ktor: 3.3.3

The only thing that worked was to force a new connection on every request (very bad) or to handle a connection as stale after it has not been used for 30s, which still result in very poor user-experience.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions