androidspring-bootroutesrsocketrsocket-java

RSocket Android + Spring Boot back-end routing error: No handler for destination ''


I get the ApplicationErrorException: No handler for destination '' trying to connet to my web server (spring boot) from android code using RSocket. As a transport I use websockets.

On the server side I use:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-rsocket</artifactId>
</dependency>

On the client I used both:

implementation 'io.rsocket:rsocket-core:1.1.1'
implementation 'io.rsocket:rsocket-transport-netty:1.1.1'

and

implementation 'io.rsocket.kotlin:rsocket-core:0.13.1'
implementation 'io.rsocket.kotlin:rsocket-transport-ktor:0.13.1'
implementation 'io.rsocket.kotlin:rsocket-transport-ktor-client:0.13.1'
implementation "io.ktor:ktor-client-cio:1.6.1"

Both Ktor and Netty had given me the same error. An error log is the following:

Android:

ApplicationErrorException (0x201): No handler for destination ''
        at io.rsocket.exceptions.Exceptions.from(Exceptions.java:76)
        at io.rsocket.core.RSocketRequester.handleFrame(RSocketRequester.java:261)
        at io.rsocket.core.RSocketRequester.handleIncomingFrames(RSocketRequester.java:211)
        at io.rsocket.core.RSocketRequester.$r8$lambda$kDn7LIfo960b6cXO3SLu8QVkTAE(Unknown Source:0)
        at io.rsocket.core.RSocketRequester$$ExternalSyntheticLambda2.accept(Unknown Source:4)
        at reactor.core.publisher.LambdaSubscriber.onNext(LambdaSubscriber.java:160)
        at io.rsocket.core.ClientServerInputMultiplexer$InternalDuplexConnection.onNext(ClientServerInputMultiplexer.java:248)
        at io.rsocket.core.ClientServerInputMultiplexer.onNext(ClientServerInputMultiplexer.java:129)
        at io.rsocket.core.ClientServerInputMultiplexer.onNext(ClientServerInputMultiplexer.java:48)
        at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120)
        at reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:365)
        at reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:401)
        at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:707)
        at reactor.netty.http.client.WebsocketClientOperations.onInboundNext(WebsocketClientOperations.java:161)
        at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:94)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.lang.Thread.run(Thread.java:764)
        Suppressed: java.lang.Exception: #block terminated with an error
        at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:99)
        at reactor.core.publisher.Mono.block(Mono.java:1703)
        at com.rsockettester.MainActivity$connect$1$1.invokeSuspend(MainActivity.kt:86)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

Here is the controller code, used on the back-end:

@Controller
class MainController {

    @MessageMapping("hello")
    fun hello() = "Hello!"

    @MessageMapping("name")
    fun helloName(name: String) = "Hello, $name!"
}

The code I used to connect from Android, using 'io.rsocket:rsocket-transport-netty:1.1.1' is the following:

    private fun connect(route: String, message: String): String? = runBlocking {
        withContext(Dispatchers.IO) {

            val ws: WebsocketClientTransport =
                WebsocketClientTransport.create(URI.create(hostUrl))
            val clientRSocket = RSocketConnector.connectWith(ws).block()
            
            return@withContext try {
                
                val compositeByteBuf = CompositeByteBuf(ByteBufAllocator.DEFAULT, false, 1);
                val routingMetadata = TaggingMetadataCodec.createRoutingMetadata(ByteBufAllocator.DEFAULT, listOf(route))
                CompositeMetadataCodec.encodeAndAddMetadata(compositeByteBuf, ByteBufAllocator.DEFAULT, 
                                        WellKnownMimeType.MESSAGE_RSOCKET_ROUTING, routingMetadata.content)
                val md = ByteBufUtil.getBytes(compositeByteBuf)
                val payload = DefaultPayload.create(message.toByteArray(), md)
                
                val s = clientRSocket?.requestResponse(payload)
                s?.block()?.dataUtf8
            } catch (e: Exception) {
                Log.e("net", "RSocket cannot connect: ", e)
                e.asString()
            } finally {
                clientRSocket?.dispose()
            }

        }
    }

The code used to connect with Ktor (as described here) is the the following:

    private fun connect(route: String, message: String): String? = runBlocking {
        val client = HttpClient(CIO) { //create and configure ktor client
            install(WebSockets)
            install(RSocketSupport) {
                connector = RSocketConnector {
                    connectionConfig {
                        payloadMimeType = PayloadMimeType(
                            data = "application/json",
                            metadata = "application/json"
                        )
                    }

                    acceptor {
                        RSocketRequestHandler {
                            requestResponse { it } //echo request payload
                        }
                    }
                }
            }
            expectSuccess = false
        }

        var rSocket: RSocket? = try {
            client.rSocket(hostUrl)
        } catch (e: Exception) {
            Log.e("net", "RSocket cannot connect:", e)
            return@runBlocking "RSocket cannot connect: ${e.asString()}"
        }

        return@runBlocking try {
            val payload = Payload(ByteReadPacket(message.toByteArray()),
                CompositeMetadata(RoutingMetadata(route)).toPacket())
            val response = rSocket?.requestResponse(payload)
            Log.d("net", "reached response")
            response?.let { it.data.readUTF8Line() }
        } catch (e: Exception) {
            e.printStackTrace()
            "RSocket cannot connect: ${e.asString()}"
        }
    }

As I mentioned above, both approaches lead to the same result: No handler for destination ''

Worth to mention, this issue is absent when I use the same routing from another Spring Boot client.

Does anyone have any clue what am I doing wrong? I would be happy if someone points me, where I'm mistaken. Thanks in advance.

I created sample projects on the github to help reproduce this error: rsocket-android-spring

Steps to reproduce:

  1. Clone or download the github project rsocket-android-spring
  2. Run the spring boot server
  3. Edit the hostUrl variable providing the the correct IP-address of your PC (!)
  4. Run the Android app and click the 'Send' button

If you wish to switch to Ktor from Netty on Android, you can use the commented method in the MainActivity code, but don't forget to use the required dependencies in build.gradle (present there).


Solution

  • The issue was in metadata setting.

    Following sample on the rsocket-kotlin I set metadata type to metadata = "application/json", though to use routes I needed it to be metadata = "message/x.rsocket.composite-metadata.v0".

    Big thanks to @haal for their detailed answer!

    Now the code to connect from Android is the following:

        private fun getPayload(route: String, message: String): Payload {
            val metadata = ByteBufAllocator.DEFAULT.compositeBuffer()
            val routingMetadata =
                TaggingMetadataCodec.createRoutingMetadata(ByteBufAllocator.DEFAULT, listOf(route))
            CompositeMetadataCodec.encodeAndAddMetadata(
                metadata,
                ByteBufAllocator.DEFAULT,
                WellKnownMimeType.MESSAGE_RSOCKET_ROUTING,
                routingMetadata.content
            )
            val data = ByteBufAllocator.DEFAULT.buffer().writeBytes(message.toByteArray())
    
            return DefaultPayload.create(data, metadata)
        }
    
        private fun connect(route: String, message: String): String? = runBlocking {
            withContext(Dispatchers.IO) {
    
                val ws: WebsocketClientTransport =
                    WebsocketClientTransport.create(URI.create(hostUrl))
                val clientRSocket = RSocketConnector.create()
                    //metadata header needs to be specified
                    .metadataMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.string)
                    // value of spring.rsocket.server.port eg 7000
                    .connect(ws)
                    .block()
                return@withContext try {
                    val s = clientRSocket?.requestResponse(getPayload(route, message))
                    s?.block()?.dataUtf8
                } catch (e: Exception) {
                    Log.e("net", "RSocket cannot connect: ", e)
                    e.asString()
                } finally {
                    clientRSocket?.dispose()
                }
    
            }
        }
    
    

    I updated the repository and the routing from the Android client to Spring Boot WebFlux server works as expected atm.

    Big thanks to RSocket developers for such an amazing tool!