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 = "")
Related
I make a request to the server, but there is no body in the response.
Accordingly, the return value type of response is Unit.
suspend fun foo(
url: String,
id: Long
) {
val requestUrl = "$url/Subscriptions?id=${id}"
val response = httpApiClient.delete<Unit>(requestUrl) {
headers {
append(HttpHeaders.Authorization, createRequestToken(token))
}
}
return response
}
How in this case to receive the code of the executed request?
HttpResponseValidator {
validateResponse { response ->
TODO()
}
}
using a similar construction and throwing an error, for example, is not an option, since one http client is used for several requests, and making a new http client for one request is strange. is there any other way out?
You can specify the HttpResponse type as a type argument instead of Unit to get an object that allows you to access the status property (HTTP status code), headers, to receive the body of a response, etc. Here is an example:
import io.ktor.client.HttpClient
import io.ktor.client.engine.apache.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
suspend fun main() {
val client = HttpClient(Apache)
val response = client.get<HttpResponse>("https://httpbin.org/get")
// the response body isn't received yet
println(response.status)
}
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
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")
}
}
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.
I would like to add a default body to a single Retrofit call inside the interface I made.
Let's say I have a Retrofit interface such as:
import retrofit2.Call
import retrofit2.http.*
interface ExampleAPI {
#POST
fun makeRequest(): Call<SomeResponse>
}
And I would like to add a default body to the request with fields such as:
param_one: j32n4n4jt
param_two: k23n45k43t
I am aware I can wrap the generated function and inject the body via:
import retrofit2.Call
import retrofit2.http.*
interface ExampleAPI {
#POST
fun makeRequest(#Body body: Map<String, String>): Call<SomeResponse>
}
or I can make a if check in an interceptor.
However, is it possible to implement this directly in the interface, and if so, how?
Try this
Generic Request:
open class CommonRequest(
#Ignore
#SerializedName("param_one") val param_one: String = "j32n4n4jt",
#Ignore
#SerializedName("param_two") val param_two: String = "j32n4n4jt"
)
Actual Request:
data class Request(
#SerializedName("name") val name: String = "Andrew",
) : CommonRequest()
Usage:
interface ExampleAPI {
#POST
fun makeRequest(#Body request: Request): Call<SomeResponse>
}