0

I am trying to implement a RESTful api with Ktor and KMongo. I connected a mongoDB Atlas database and it works well.

Then I prepared to deploy a first version of the api on Heroku and now when I run it locally every query to the database throws this exception:

2021-01-07 15:10:30.211 [eventLoopGroupProxy-4-2] INFO  org.mongodb.driver.cluster - Cluster created with settings {hosts=[127.0.0.1:27017], srvHost=cluster0.euib1.mongodb.net, mode=MULTIPLE, requiredClusterType=REPLICA_SET, serverSelectionTimeout='30000 ms', maxWaitQueueSize=500, requiredReplicaSetName='atlas-hl2rus-shard-0'}
15:10:30 web.1   |  2021-01-07 15:10:30.258 [cluster-ClusterId{value='5ff7165648f4085f7af773f9', description='null'}-srv-cluster0.euib1.mongodb.net] INFO  org.mongodb.driver.cluster - Adding discovered server cluster0-shard-00-00.euib1.mongodb.net:27017 to client view of cluster
15:10:30 web.1   |  2021-01-07 15:10:30.268 [eventLoopGroupProxy-4-2] ERROR Application - Unhandled: GET - /books
15:10:30 web.1   |  java.lang.NullPointerException: Cannot invoke "com.mongodb.connection.ClusterDescription.getConnectionMode()" because "clusterDescription" is null
15:10:30 web.1   |      at com.mongodb.async.client.ClientSessionHelper.getServerDescriptionListToConsiderForSessionSupport(ClientSessionHelper.java:107)
15:10:30 web.1   |      at com.mongodb.async.client.ClientSessionHelper.createClientSession(ClientSessionHelper.java:63)
15:10:30 web.1   |      at com.mongodb.async.client.ClientSessionHelper.withClientSession(ClientSessionHelper.java:51)
15:10:30 web.1   |      at com.mongodb.async.client.OperationExecutorImpl.execute(OperationExecutorImpl.java:66)
15:10:30 web.1   |      at com.mongodb.async.client.MongoIterableImpl.batchCursor(MongoIterableImpl.java:161)
15:10:30 web.1   |      at com.mongodb.async.client.MongoIterableSubscription.requestInitialData(MongoIterableSubscription.java:46)
15:10:30 web.1   |      at com.mongodb.async.client.AbstractSubscription.tryRequestInitialData(AbstractSubscription.java:151)
15:10:30 web.1   |      at com.mongodb.async.client.AbstractSubscription.request(AbstractSubscription.java:84)
15:10:30 web.1   |      at com.mongodb.reactivestreams.client.internal.ObservableToPublisher$1$1.request(ObservableToPublisher.java:50)
15:10:30 web.1   |      at kotlinx.coroutines.reactive.SubscriptionChannel.onReceiveEnqueued(Channel.kt:64)
15:10:30 web.1   |      at kotlinx.coroutines.channels.AbstractChannel.enqueueReceive(AbstractChannel.kt:592)
15:10:30 web.1   |      at kotlinx.coroutines.channels.AbstractChannel.access$enqueueReceive(AbstractChannel.kt:488)
15:10:30 web.1   |      at kotlinx.coroutines.channels.AbstractChannel$Itr.hasNextSuspend(AbstractChannel.kt:841)
15:10:30 web.1   |      at kotlinx.coroutines.channels.AbstractChannel$Itr.hasNext(AbstractChannel.kt:827)
15:10:30 web.1   |      at org.litote.kmongo.coroutine.CoroutinePublisherKt.toList(CoroutinePublisher.kt:56)
15:10:30 web.1   |      at org.litote.kmongo.coroutine.CoroutinePublisher.toList(CoroutinePublisher.kt:46)
15:10:30 web.1   |      at com.flockware.BookControllerKt$booksRoutes$1$1.invokeSuspend(BookController.kt:29)
15:10:30 web.1   |      at com.flockware.BookControllerKt$booksRoutes$1$1.invoke(BookController.kt)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:243)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:113)
15:10:30 web.1   |      at io.ktor.features.StatusPages$interceptCall$2.invokeSuspend(StatusPages.kt:102)
15:10:30 web.1   |      at io.ktor.features.StatusPages$interceptCall$2.invoke(StatusPages.kt)
15:10:30 web.1   |      at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91)
15:10:30 web.1   |      at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:189)
15:10:30 web.1   |      at io.ktor.features.StatusPages.interceptCall(StatusPages.kt:101)
15:10:30 web.1   |      at io.ktor.features.StatusPages$Feature$install$2.invokeSuspend(StatusPages.kt:142)
15:10:30 web.1   |      at io.ktor.features.StatusPages$Feature$install$2.invoke(StatusPages.kt)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:243)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:113)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.execute(SuspendFunctionGun.kt:133)
15:10:30 web.1   |      at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77)
15:10:30 web.1   |      at io.ktor.routing.Routing.executeResult(Routing.kt:151)
15:10:30 web.1   |      at io.ktor.routing.Routing.interceptor(Routing.kt:35)
15:10:30 web.1   |      at io.ktor.routing.Routing$Feature$install$1.invokeSuspend(Routing.kt:103)
15:10:30 web.1   |      at io.ktor.routing.Routing$Feature$install$1.invoke(Routing.kt)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:243)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:113)
15:10:30 web.1   |      at io.ktor.features.ContentNegotiation$Feature$install$1.invokeSuspend(ContentNegotiation.kt:110)
15:10:30 web.1   |      at io.ktor.features.ContentNegotiation$Feature$install$1.invoke(ContentNegotiation.kt)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:243)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:113)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.execute(SuspendFunctionGun.kt:133)
15:10:30 web.1   |      at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77)
15:10:30 web.1   |      at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$2.invokeSuspend(DefaultEnginePipeline.kt:118)
15:10:30 web.1   |      at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$2.invoke(DefaultEnginePipeline.kt)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:243)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:113)
15:10:30 web.1   |      at io.ktor.util.pipeline.SuspendFunctionGun.execute(SuspendFunctionGun.kt:133)
15:10:30 web.1   |      at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77)
15:10:30 web.1   |      at io.ktor.server.netty.NettyApplicationCallHandler$handleRequest$1.invokeSuspend(NettyApplicationCallHandler.kt:118)
15:10:30 web.1   |      at io.ktor.server.netty.NettyApplicationCallHandler$handleRequest$1.invoke(NettyApplicationCallHandler.kt)
15:10:30 web.1   |      at kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(Undispatched.kt:55)
15:10:30 web.1   |      at kotlinx.coroutines.BuildersKt__Builders_commonKt.startCoroutineImpl(Builders.common.kt:182)
15:10:30 web.1   |      at kotlinx.coroutines.BuildersKt.startCoroutineImpl(Unknown Source)
15:10:30 web.1   |      at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:145)
15:10:30 web.1   |      at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:54)
15:10:30 web.1   |      at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
15:10:30 web.1   |      at io.ktor.server.netty.NettyApplicationCallHandler.handleRequest(NettyApplicationCallHandler.kt:43)
15:10:30 web.1   |      at io.ktor.server.netty.NettyApplicationCallHandler.channelRead(NettyApplicationCallHandler.kt:34)
15:10:30 web.1   |      at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
15:10:30 web.1   |      at io.netty.channel.AbstractChannelHandlerContext.access$600(AbstractChannelHandlerContext.java:61)
15:10:30 web.1   |      at io.netty.channel.AbstractChannelHandlerContext$7.run(AbstractChannelHandlerContext.java:370)
15:10:30 web.1   |      at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
15:10:30 web.1   |      at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
15:10:30 web.1   |      at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:500)
15:10:30 web.1   |      at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
15:10:30 web.1   |      at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
15:10:30 web.1   |      at io.ktor.server.netty.EventLoopGroupProxy$Companion$create$factory$1$1.run(NettyApplicationEngine.kt:216)
15:10:30 web.1   |      at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
15:10:30 web.1   |      at java.base/java.lang.Thread.run(Thread.java:832)

I connect the database in this way:

KMongo.createClient("mongodb+srv://<username>:<password>@cluster0.euib1.mongodb.net/<dbname>?retryWrites=true&w=majority").coroutine

My Gradle file:

buildscript {
    repositories {
        jcenter()
    }
    
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
    }
}

apply plugin: 'kotlin'
apply plugin: 'application'

group 'com.example'
version '0.0.1'
mainClassName = 'com.example.ApplicationKt'

apply plugin: 'com.github.johnrengelman.shadow'

shadowJar {
    manifest {
        attributes 'Main-Class': mainClassName
    }
}

sourceSets {
    main.kotlin.srcDirs = main.java.srcDirs = ['src']
    test.kotlin.srcDirs = test.java.srcDirs = ['test']
    main.resources.srcDirs = ['resources']
    test.resources.srcDirs = ['testresources']
}

repositories {
    mavenLocal()
    jcenter()
    maven { url 'https://kotlin.bintray.com/ktor' }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation "io.ktor:ktor-server-netty:$ktor_version"
    implementation "ch.qos.logback:logback-classic:$logback_version"
    implementation "io.ktor:ktor-server-core:$ktor_version"
    implementation "io.ktor:ktor-server-host-common:$ktor_version"
    implementation "io.ktor:ktor-jackson:$ktor_version"
    implementation "io.ktor:ktor-locations:$ktor_version"
    implementation "io.ktor:ktor-metrics:$ktor_version"
    testImplementation "io.ktor:ktor-server-tests:$ktor_version"


    implementation 'org.litote.kmongo:kmongo-coroutine:3.10.0'
    implementation group: 'org.koin', name: 'koin-ktor', version: '2.0.0-beta-1'
    implementation group: 'org.koin', name: 'koin-core-ext', version: '2.0.0-beta-1'
    testImplementation group: 'org.koin', name: 'koin-test', version: '2.0.0-beta-1'
}

This is ApplicationKt.kt:

fun main(args: Array<String>) {
    embeddedServer(Netty, commandLineEnvironment(args)).start(true)
}

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
    install(CORS) {
        method(HttpMethod.Options)
        method(HttpMethod.Post)
        method(HttpMethod.Put)
        method(HttpMethod.Delete)
        method(HttpMethod.Patch)
        header(HttpHeaders.Authorization)
        header("MyCustomHeader")
        allowCredentials = true
        anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
    }

    install(ContentNegotiation) {
        jackson {
            enable(SerializationFeature.INDENT_OUTPUT)
        }
    }

    install(Locations) {}

    installKoin {
        modules(mongoModule)
    }

    routing {
        
        booksRoutes()

        install(StatusPages) {
            exception<AuthenticationException> { cause ->
                call.respond(HttpStatusCode.Unauthorized)
            }
            exception<AuthorizationException> { cause ->
                call.respond(HttpStatusCode.Forbidden)
            }

        }
    }
}

class AuthenticationException : RuntimeException()
class AuthorizationException : RuntimeException()

And these are routes:

fun Route.booksRoutes() {
    val logger: Logger = LoggerFactory.getLogger("BooksController")
    val client: CoroutineClient by inject()

    route("/books") {
        get("") {
            val books = client.getDatabase(DB_NAME)
                .getCollection<Book>(BOOKS_COLLECTION)
                .find()
                .toList()
            call.respond(HttpStatusCode.OK, books)
        }

        post<Book>("") { request ->
            logger.debug("$request")
            client.getDatabase(DB_NAME)
                    .getCollection<Book>(BOOKS_COLLECTION)
                    .insertOne(request)
            call.respond(HttpStatusCode.OK)
        }

        get("/{bookId}") {
            val bookId:String = call.parameters["bookId"]!!
            val book = client.getDatabase(DB_NAME)
                    .getCollection<Book>(BOOKS_COLLECTION)
                    .findOneById(bookId)
            if (book == null)
                call.respond(HttpStatusCode.NotFound)
            else
                call.respond(HttpStatusCode.OK, book)
        }
    }
}

This is my first time working with MongoDB, so it could be a stupid error. But a can't understand if the problem is in my code or in MongoDB Atlas config.

Rolud
  • 121
  • 1
  • 8

1 Answers1

0

I had a similar experience with connecting to the mongo cloud with a standard connection string. In the end, what worked was creating an X.509 certificate on the atlas cloud then using that to connection with a connection string like this:

mongodb+srv://mflix.beal2.mongodb.net/sample_mflix?**authMechanism=MONGODB-X509**&replicaSet=atlas-a7tqy4-shard-0&connectTimeoutMS=7000&maxPoolSize=50&wTimeoutMS=2500&readPreference=primary&**authSource=%24external**

You need to load the X.509 certificate into a keystone

openssl pkcs12 -export -out mongodb.pkcs12 -in X509-cert.pem

Then ensure that the keystore is accessible to your process:

javax.net.ssl.keyStore=./mongodb.pkcs12
javax.net.ssl.keyStorePassword=PWD

I go into this in a bit more detail here in this post:
mongo-loves-data.medium.com/kotlin-coroutines-and-ktor

Antoine
  • 1,393
  • 4
  • 20
  • 26
Ilan Toren
  • 50
  • 6