Ktor is throwing a "Lost in ambiguity tie" error on GET requests - kotlin

I have a project that was running on Kotlin 1.4.0 and Ktor 1.3.2, so I decided to try and upgrade my versions to Kotlin 1.4.30 and Ktor 1.5.1 (since I was also upgrading to Java 11 due to adoptopenJDK8 not having JavaFx).
When I finally fixed import issues and whatnot and ran my webserver, the first page opens correctly (it's mapped to route "/"), but the other GET requests throw a "Lost in ambiguity tie" error on the RoutingResolve class. Apparently when comparing the quality of registered selectors, the "/ping" one has the same value as "//", hence throwing the error. I have validated this behaviour Ktor 1.5.1 and 1.5.0. Versions 1.4.x throw coroutine errors.
My server configurations are declared as such:
fun serverConfiguration(controller: Controller): NettyApplicationEngine {
SocketRoutes.controller = controller
WebUIRoutes.controller = controller
return embeddedServer(Netty, 9090) {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
dateFormat = DateFormat.getDateInstance()
deactivateDefaultTyping()
}
}
install(FreeMarker) {
templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates")
defaultEncoding = Charsets.UTF_8.toString()
}
install(CallLogging)
install(Sessions) {
cookie<UserPreferences>("preferences")
}
getRoutes(this)
}
}
My Routes.kt
fun getRoutes(pipeline: Application): Routing {
return Routing.install(pipeline) {
trace { application.log.trace(it.buildText()) }
static("/") {
resources("web-static")
}
home()
getTableBody()
ping()
}
}
And these routes themselves are on WebUIRoutes.kt
object WebUIRoutes {
lateinit var controller: Controller
fun Route.home() {
get("/") {
val session = call.sessions.get<UserPreferences>() ?: UserPreferences()
call.sessions.set(session)
val nrPages = calculateNrOfPages(session.nrElements)
call.response.cookies["nrElements"]?.copy(value = "$nrPages")
call.respondTemplate(
"showCodes.ftl", mapOf(
"total" to nrPages, "nrElements" to session.nrElements,
"validateOnInput" to session.validateOnInput
)
)
}
}
(...)
fun Route.ping() {
get("/ping") {
if (controller.ping())
call.respond(HttpStatusCode.OK)
else
call.respond(HttpStatusCode.NotFound)
}
}
}
So I have no idea what the hell is going on and I've found nothing online about migrating to newer versions of Ktor and related issues...

The problem is that the resources function adds a route with a wildcard under the / route and therefore the routing resolves it instead of the /ping route. You can just move the static function call to the end of Routing to make it work as you expect.

Related

Mocking internal function call in Kotlin

I am a complete beginner in terms of Kotlin and I am finding some issues while trying to test out a Ktor based application.
I have a file in my endpoints package localized at org.example.endpoints.hello
this file contains a fun Application.hello that implements an endpoint for my application.
This endpoint acts as a wrapper for another API, so inside that same file I have a
fun callOtherAPI(): ResponseContainer {
// networking stuff
return ResponseContainer(message: "some stuff")
}
This function gets called inside the Application's function routing implementation as such:
routing {
get("/hello") {
call.respond(callOtherAPI())
}
}
Now to the issue:
My test currently looks like this:
#Test
fun testHello() = testApplication {
application {
hello()
}
mockkStatic(::callOtherAPI)
every { callOtherAPI() } returns ResponseContainer("hello")
print(callOtherAPI()) // This actually returns the mocked response, which is what I want
client.get("/hello").apply {
val expected = ResponseContainer("hello")
val response = jacksonObjectMapper().readValue<ResponseContainer>(bodyAsText())
assertEquals(HttpStatusCode.OK, status)
assertEquals(expected.message, response.message) // This assert fails because the internal call to callOtherAPI() is not being mocked.
}
}
So the problem that I am facing is that while the mocked function is being mocked within the context of the test, it is not being mocked when called internally by the routing implementation.
Can someone point me to good documentation to figure this out, I've been at it for the past two hours to no avail :/
Thanks!
You can declare a parameter for the callOtherAPI function in the hello method. For the production and testing environment you will pass different functions in this case. Here is your code rewritten:
#Test
fun testHello() = testApplication {
application {
// hello(::callOtherAPI) this call will be for the production environment
hello { ResponseContainer("hello") }
}
client.get("/hello").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals("{\"message\":\"hello\"}", bodyAsText())
}
}
data class ResponseContainer(val message: String)
fun Application.hello(callOtherAPI: () -> ResponseContainer) {
install(ContentNegotiation) {
jackson()
}
routing {
get("/hello") {
call.respond(callOtherAPI())
}
}
}
fun callOtherAPI(): ResponseContainer {
// networking stuff
return ResponseContainer("some stuff")
}

in ktor plugin "Serialization" don't work with plug in "FreeMaker"

ok I'm new to Ktor and I'm learning about it, one thing that I faced in this progress was this error :
kotlinx.serialization.SerializationException: Serializer for class 'FreeMarkerContent' is not found.
Mark the class as #Serializable or provide the serializer explicitly
and thats becouse in my code, Im using FreeMakcerContent and its trying to Serlialize it :
get {
call.respond(FreeMarkerContent("index.ftl", mapOf("articles" to articles)))
}
how can I fix this problem?
I faced the same problem and I fixed it by installing FreeMarker plugin before installing ContentNegotiation plugin.
install(FreeMarker) {
templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates")
}
install(ContentNegotiation) {
json()
}
routing {
get("/html-freemarker") {
call.respond(FreeMarkerContent("index.ftl", mapOf("name" to "Shalaga"), ""))
}
}
ok I think I found the solution, in the configuration of Serialization I did define the route that i need sterilization in it ( my Http Api route), something like this
fun Application.configureSerialization() {
routing {
route("api"){
install(ContentNegotiation) {
json()
}
}
}
}
so the Serialization will only work in this route

How can the status code for a response provided by a #ExceptionHandler be set when the #Controller returns a reactive type (Mono)?

How can the status code for a response provided by a #ExceptionHandler be set when the #Controller returns a reactive type (Mono)?
It seems that it is not possible via returning a ResponseEntity or annotating the #ExceptionHandler method with #ResponseStatus.
A fairly minimal test showing the issue (note that the response body and content type are correctly verified while the status code is OK when it should be INTERNAL_SERVER_ERROR):
class SpringWebMvcWithReactiveResponseTypeExceptionHandlerCheckTest {
#RestController
#RequestMapping("/error-check", produces = ["text/plain;charset=UTF-8"])
class ExceptionHandlerCheckController {
#GetMapping("errorMono")
fun getErrorMono(): Mono<String> {
return Mono.error(Exception())
}
}
#ControllerAdvice
class ErrorHandler : ResponseEntityExceptionHandler() {
#ExceptionHandler(Exception::class)
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
fun handleException(ex: Exception): ResponseEntity<*> = ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(mapOf("key" to "value"))
}
val mockMvc = MockMvcBuilders
.standaloneSetup(ExceptionHandlerCheckController())
.setControllerAdvice(ErrorHandler())
.build()
#Test
fun `getErrorMono returns HTTP Status OK instead of the one set by an ExceptionHandler`() {
mockMvc.get("/error-check/errorMono")
// .andExpect { status { isInternalServerError() } }
.andExpect { status { isOk() } }
.asyncDispatch()
.andExpect {
content {
contentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE)
json("""
{
"key": "value"
}
""",strict = true
)
}
}
}
}
(build.gradle.kts excerpt showing relevant dependencies):
plugins: {
id("org.springframework.boot") version "2.4.5"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
The issue is that .andExpect { status ... has to be called after .asyncDispatch() (as was done with content type and body). It seems that the http status is updated. Possibly this is actually part of http standard for async requests, but I suspect this is a bug of the underlying MockHttpServletResponse.

Kotlin multiplatform: JobCancellationException: Parent job is Completed

I try to write a kotlin multiplatform library (android and ios) that uses ktor. Thereby I experience some issues with kotlins coroutines:
When writing tests I always get kotlinx.coroutines.JobCancellationException: Parent job is Completed; job=JobImpl{Completed}#... exception.
I use ktors mock engine for my tests:
client = HttpClient(MockEngine)
{
engine
{
addHandler
{ request ->
// Create response object
}
}
}
A sample method (commonMain module) using ktor. All methods in my library are written in a similar way. The exception occures if client.get is called.
suspend fun getData(): Either<Exception, String> = coroutineScope
{
// Exception occurs in this line:
val response: HttpResponse = client.get { url("https://www.google.com") }
return if (response.status == HttpStatusCode.OK)
{
(response.readText() as T).right()
}
else
{
Exception("Error").left()
}
}
A sample unit test (commonTest module) for the above method. The assertTrue statement is never called since the exception is thrown before.
#Test
fun getDataTest() = runTest
{
val result = getData()
assertTrue(result.isRight())
}
Actual implementation of runTest in androidTest and iosTest modules.
actual fun<T> runTest(block: suspend () -> T) { runBlocking { block() } }
I thought when I use coroutineScope, it waits until all child coroutines are done. What am I doing wrong and how can I fix this exception?
you can't cache HttpClient of CIO in client variable and reuse, It would be best if change the following code in your implementation.
val client:HttpClient get() = HttpClient(MockEngine) {
engine {
addHandler { request ->
// Create response object
}
}
}
The library must be updated, this glitch is in the fix report here: https://newreleases.io/project/github/ktorio/ktor/release/1.6.1
The problem is that you cannot use the same instance of the HttpClient. My ej:
HttpClient(CIO) {
install(JsonFeature) {
serializer = GsonSerializer()
}
}.use { client ->
return#use client.request("URL") {
method = HttpMethod.Get
}
}

Kotlin Coroutine - Ktor Server WebSocket

I made a kotlin-ktor application, what i wanted to achieve is that it is modular that anytime any pipelines inside the application maybe removed from the source code without breaking any functionalities. So i decided i want to move a websocket implementation to separate class
but i faced an issue where the coroutine inside the lambda expression terminates immediately.
link-github issue
Can someone enlighten me about the coroutine setup on this, and how I can still keep this as modular without this kind of issue
working ktor websocket
fun Application.socketModule() = runBlocking {
// other declarations
......
routing {
val sessionService = SocketSessionService()
webSocket("/home") {
val chatSession = call.sessions.get<ChatSession>()
println("request session: $chatSession")
if (chatSession == null) {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "empty Session"))
return#webSocket
}
send(Frame.Text("connected to server"))
sessionService.addLiveSocket(chatSession.id, this)
sessionService.checkLiveSocket()
}
thread(start = true, name = "socket-monitor") {
launch {
sessionService.checkLiveSocket()
}
}
}
}
kotlin-ktor auto-close web socket
code below closes the socket automatically
Socket Module
class WebSocketServer {
fun createWebSocket(root: String, routing: Routing) {
println("creating web socket server")
routing.installSocketRoute(root)
}
private fun Routing.installSocketRoute(root: String) {
val base = "/message/so"
val socketsWeb = SocketSessionService()
webSocket("$root$base/{type}") {
call.parameters["type"] ?: throw Exception("missing type")
val session = call.sessions.get<ChatSession>()
if (session == null) {
println( "WEB-SOCKET:: client session is null" )
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "No Session"))
return#webSocket
}
socketsWeb.addLiveSocket(session.id, this)
thread(start= true, name = "thread-live-socket") {
launch {
socketsWeb.checkLiveSocket()
}
}
}
}
}
Application Module
fun Application.socketModule() = runBlocking {
// other delcarations
.....
install(Sessions) {
cookie<ChatSession>("SESSION")
}
intercept(ApplicationCallPipeline.Features) {
if (call.sessions.get<ChatSession>() == null) {
val sessionID = generateNonce()
println("generated Session: $sessionID")
call.sessions.set(ChatSession(sessionID))
}
}
routing {
webSocketServer.createWebSocket("/home", this)
}
}
I quite dont understand why the coroutine insdie webSocket lamda is completed.
Can someone show me other/right approach on this one.