Get request to Ktor Location results in an Unhandled request - kotlin

I just want to use the Locations Feature of Ktor for my API and I tested it with two simple get requests. The problem is that Ktor seems to ignore my #Locations and if I use the Logging feature it says that the request is unhandled:
TRACE Application - Unhandled: GET - /register
My Application class:
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.gson.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
fun Application.main() {
install(Locations)
install(CallLogging)
install(ContentNegotiation) {
gson {
setPrettyPrinting()
}
}
routing {
get("/health_check") {
call.respondText("OK")
}
auth()
}
}
Auth class:
import io.ktor.application.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
#Location("/register")
data class Register(val username: String, val email: String, val password: String)
#Location("/login")
data class Login(val username: String, val password: String)
fun Route.auth() {
get<Register> {
call.respondText("Register")
}
get<Login> {
call.respondText("Login")
}
}
I tried to find some information about my problem, but I haven't found any. I thought it might have something to do with the fact that the Locations API of Ktor is still experimental. I hope this is enough information. Thanks in advance.

Related

Ktor with Koin DI can't inject, missing clazz

I am setting up Koin DI on Ktor in this way:
https://insert-koin.io/docs/reference/koin-ktor/ktor/
But I am getting an error: No value passed for parameter 'clazz'
My implementation looks like this:
import io.ktor.application.*
import io.ktor.routing.*
import org.koin.java.KoinJavaComponent.inject
import services.SomeService
fun Application.registerPropertyRoutes() {
routing {
bodySectionRoute() // add more routes for Property page here
}
}
fun Route.bodySectionRoute() {
val someService by inject<SomeService>()
get("/bodySection") {
// business logic can be connected here
}
}
Any ideas what I am missing?
Update:
You need to import org.koin.ktor.ext.inject

Ktor Location API: mapping JSON to generic Map object

When I use the regular routing API together with GSON, I can deserialize a JSON parameter to a Map<String, Any> with the following code snippet:
post("/books") {
val request = call.receive<Map<String, Any>>()
...
}
In my case request is an instance of com.google.gson.internal.LinkedTreeMap.
Is there a way to do the same using the Location API? It works fine when I define a data class with concrete members but I can't find a way to use a map. I'm trying it with a couple of things along those lines:
#Location ("/books")
<some magic class definition>
fun Application.bookModule() {
routing {
post<BookRequest> {
val request = call.receive<BookRequest>()
...
}
but I've not come up with anything that works. Help?
Locations represent path and query parameters of endpoints only, so there is no way to route depending on the request body. Properties of a location class should be mapped to path segments, e.g.
#Location ("/books/{title}/{author}") data class BookRequest(
val title: String,
val author: String,
)
To receive a request body and convert to it an object just use the ContentNegotiation plugin. Here is the example of a server that responds with 200 OK to the curl -v --header "Content-Type: application/json" --request POST --data '{"title":"Hitchhiker", "author":"DNA", "detail":{"foo": "bar"}}' http://localhost:8080/books request:
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.gson.*
import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.locations.post
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.routing
import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
gson()
}
install(Locations)
bookModule()
}.start()
}
fun Application.bookModule() {
routing {
post<BookRequest> {
val request = call.receive<Map<String, Any>>()
println(request)
call.respond(HttpStatusCode.OK)
}
}
}
#Location ("/books") class BookRequest

Sample for REST API using Location and JSON

The title already says what I'm trying to do in a nutshell: implement a simple REST API that uses ktor's Location feature and accepts requests with JSON as payload.
Let's say that I want to have a resource "books" that is available under the URI /books. A get request should return a list of available books, a post request with data for a new book should create a new book and return a redirect to the newly created book. My implementation looks like this:
#Location ("/books") data class BookRequest(val title: String = "" )
fun Application.bookModule() {
routing {
get<BookRequest> {
val books = bookHandler.listBooks()
//generate HTML from books and respond
}
post<BookRequest> {
val request = call.receive<BookRequest>()
//create a new book resource from the data in request
//and respond with redirect to new book
}
}
The get request works as intended but when I try to POST a new book using curl like this
curl --header "Content-Type: application/json" \
--request POST \
--data '{"title":"Hitchhiker"}' \
http://localhost:8080/books
the content of the title attribute of the request is empty.
Does anyone have a pointer to a working example using Locations with POST and JSON?
You need to install ContentNegotiation plugin for deserializing JSON data to a BookRequest object and if you use kotlinx.serialization then mark the BookRequest class with Serializable annotation. Here is the full example:
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.locations.*
import io.ktor.locations.post
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.routing
import io.ktor.serialization.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.serialization.Serializable
fun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
json()
}
install(Locations)
routing {
post<BookRequest> {
val r = call.receive<BookRequest>()
call.respondText { r.title }
}
}
}.start()
}
#Location("/books")
#Serializable
data class BookRequest(val title: String = "")

Is there any way to make a fake call from Ktor to itself, to make request pass through all pipeline?

I have an ktor web server that successfully responds on http requests. Now there is a need to read data from kafka's topic and process it.
Is there any way send the data I've read to ktor, like this data came from outside, to make it pass through all pipeline, like ContentNegotiation and other features?
Application class has method execute(), which takes ApplicationCall, but I've found zero examples - how can I fill my implementation of this class properly. Especially route - do I need the real one? Would be nice if this route would be private and would be unavailable from the outside.
You can use the withTestApplication function to test your application's modules without making an actual network connection. Here is an example:
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class SimpleTest {
#Test
fun test() = withTestApplication {
application.module()
// more modules to test here
handleRequest(HttpMethod.Post, "/post") {
setBody("kafka data")
}.response.let { response ->
assertEquals("I get kafka data", response.content)
}
}
}
fun Application.module() {
routing {
post("/post") {
call.respondText { "I get ${call.receiveText()}" }
}
}
}
I think that #AlekseiTirman answer is great and most probably you should go for it
But I have to mention that it's easy to do it even in "real life" run. Your local machine ip is 0.0.0.0, you can get port from the env variable, so you just can create a simple HttpClient and send a request:
CoroutineScope(Dispatchers.IO).launch {
delay(1000)
val client = HttpClient {
defaultRequest {
// actually this is already a default value so no need to setting it
host = "0.0.0.0"
port = environment.config.property("ktor.deployment.port").getString().toInt()
}
}
val result = client.get<String>("good")
println("local response $result")
}
routing {
get("good") {
call.respond("hello world")
}
}

How can I configure a JAX-RS application in Kotlin with resources having other dependencies injected

I use JAX-RS to build a REST API. To bootstrap all resources I have a overridden an "Application":
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
#ApplicationScoped
#ApplicationPath("/")
open class ApiConfig : Application() {
override fun getSingletons(): MutableSet<Any> {
println("----- init jaxrs -----")
return mutableSetOf(EchoResource())
}
}
As you can see I register the EchoResource() with brackets. It does not work when I use EchoResource::class.
My Problem is, that I want to inject some service into EchoResource:
import dev.elysion.mail.smtp.MailService
import javax.enterprise.context.RequestScoped
import javax.inject.Inject
import javax.ws.rs.GET
import javax.ws.rs.Path
#Path("/echo")
#RequestScoped
class EchoResource #Inject constructor(private val mailService: MailService) {
#GET
fun getHello(): String {
return "Hello World"
}
}
When I add the constructor I get an error in API Config saying that I do not pass a parameter for MailService.
In Java I would register the resource with EchoResource.class which does not care about any parameters.
How can I achieve the same with Kotlin?
It works if getClasses() is used instead of getSingletons():
import javax.enterprise.context.ApplicationScoped
import javax.ws.rs.ApplicationPath
import javax.ws.rs.core.Application
#ApplicationScoped
#ApplicationPath("/")
open class ApiConfig : Application() {
override fun getClasses(): MutableSet<Class<*>> {
return mutableSetOf(EchoResource::class.java)
}
}
Furthermore, all resources and all of their methods need to be open (not final) in order to be proxyable for CDI.