I have an issue with catching data from request body. I know this might be very easy question but I have looking for that in google and ktor docs and havent find solution.
fun main(args: Array<String>) {
val server = embeddedServer(Netty, port = 8080, module = Application::mainModule)
server.start()
}
fun Application.mainModule() {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
}
}
routing {
post("/") {
val parameters = call.receiveParameters()
println(parameters)
}
}
}
Sending body in request:
{
"nick": "xxx"
}
Errors:
ERROR ktor.application - Unhandled: POST - /
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `io.ktor.http.Parameters` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (InputStreamReader); line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1589)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1055)
at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:265)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4202)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3218)
at io.ktor.jackson.JacksonConverter.convertForReceive(JacksonConverter.kt:44)
at io.ktor.features.ContentNegotiation$Feature$install$3.invokeSuspend(ContentNegotiation.kt:169)
at io.ktor.features.ContentNegotiation$Feature$install$3.invoke(ContentNegotiation.kt)
at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:318)
at io.ktor.util.pipeline.SuspendFunctionGun.proceed(PipelineContext.kt:163)
try
routing {
post("/") {
val parameters = call.receive<String>()
println(parameters)
}
}
You are using call.receiveParameters, but are passing a json body. =p
Related
I have this dependencies:
implementation("io.ktor:ktor-server-core:$ktor_version")
implementation("io.ktor:ktor-jackson:$ktor_version")
implementation("io.ktor:ktor-server-netty:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-client-cio:$ktor_version")
implementation("io.ktor:ktor-client-json:$ktor_version")
And this settings of ktor server:
fun Application.configureHTTP() {
install(DefaultHeaders)
install(CallLogging)
install(AutoHeadResponse)
install(Routing)
install(ContentNegotiation) {
register(ContentType.Application.Json, JacksonConverter())
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
writerWithDefaultPrettyPrinter()
}
}
install(CORS) {
method(HttpMethod.Options)
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.
}
}
I'd like to get joke's text about Chuck Norris, so I made this data classes:
data class ChuckNorrisJoke(
val type: String,
val value: Map<Any, Any>
)
data class JokeContent(
val id: Long,
val joke: String,
val categories: List<String>
)
And eventually this is my function for getting joke:
val client = HttpClient(CIO) {
install(JsonFeature)
}
suspend fun getChuckNorrisJoke(): ChuckNorrisJoke {
return client
.get("http://api.icndb.com/jokes/random")
}
When I call the method, I get this error:
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.IllegalStateException: Fail to find serializer. Consider to add one of the following dependencies:
- ktor-client-gson
- ktor-client-json
- ktor-client-serialization
at io.ktor.client.features.json.DefaultJvmKt.defaultSerializer(DefaultJvm.kt:14)
at io.ktor.client.features.json.JsonFeature$Feature.prepare(JsonFeature.kt:130)
at io.ktor.client.features.json.JsonFeature$Feature.prepare(JsonFeature.kt:125)
at io.ktor.client.HttpClientConfig$install$3.invoke(HttpClientConfig.kt:77)
at io.ktor.client.HttpClientConfig$install$3.invoke(HttpClientConfig.kt:74)
at io.ktor.client.HttpClientConfig.install(HttpClientConfig.kt:97)
at io.ktor.client.HttpClient.<init>(HttpClient.kt:172)
at io.ktor.client.HttpClient.<init>(HttpClient.kt:81)
at io.ktor.client.HttpClientKt.HttpClient(HttpClient.kt:43)
at com.example.ApplicationKt.<clinit>(Application.kt:109)
Can't understand how to set HttpClient correctly.
The implementation("io.ktor:ktor-jackson:$ktor_version") dependency declaration is for the server. You need to declare one for the client: implementation "io.ktor:ktor-client-jackson:$ktor_version". You can find more information here.
Is there any way to log the request and response body from the ktor server communication?
The buildin CallLogging feature only logs the metadata of a call. I tried writing my own logging feature like in this example: https://github.com/Koriit/ktor-logging/blob/master/src/main/kotlin/korrit/kotlin/ktor/features/logging/Logging.kt
class Logging(private val logger: Logger) {
class Configuration {
var logger: Logger = LoggerFactory.getLogger(Logging::class.java)
}
private suspend fun logRequest(call: ApplicationCall) {
logger.info(StringBuilder().apply {
appendLine("Received request:")
val requestURI = call.request.path()
appendLine(call.request.origin.run { "${method.value} $scheme://$host:$port$requestURI $version" })
call.request.headers.forEach { header, values ->
appendLine("$header: ${values.firstOrNull()}")
}
try {
appendLine()
appendLine(String(call.receive<ByteArray>()))
} catch (e: RequestAlreadyConsumedException) {
logger.error("Logging payloads requires DoubleReceive feature to be installed with receiveEntireContent=true", e)
}
}.toString())
}
private suspend fun logResponse(call: ApplicationCall, subject: Any) {
logger.info(StringBuilder().apply {
appendLine("Sent response:")
appendLine("${call.request.httpVersion} ${call.response.status()}")
call.response.headers.allValues().forEach { header, values ->
appendLine("$header: ${values.firstOrNull()}")
}
when (subject) {
is TextContent -> appendLine(subject.text)
is OutputStreamContent -> appendLine() // ToDo: How to get response body??
else -> appendLine("unknown body type")
}
}.toString())
}
/**
* Feature installation.
*/
fun install(pipeline: Application) {
pipeline.intercept(ApplicationCallPipeline.Monitoring) {
logRequest(call)
proceedWith(subject)
}
pipeline.sendPipeline.addPhase(responseLoggingPhase)
pipeline.sendPipeline.intercept(responseLoggingPhase) {
logResponse(call, subject)
}
}
companion object Feature : ApplicationFeature<Application, Configuration, Logging> {
override val key = AttributeKey<Logging>("Logging Feature")
val responseLoggingPhase = PipelinePhase("ResponseLogging")
override fun install(pipeline: Application, configure: Configuration.() -> Unit): Logging {
val configuration = Configuration().apply(configure)
return Logging(configuration.logger).apply { install(pipeline) }
}
}
}
It works fine for logging the request body using the DoubleReceive plugin. And if the response is plain text i can log the response as the subject in the sendPipeline interception will be of type TextContent or like in the example ByteArrayContent.
But in my case i am responding a data class instance with Jackson ContentNegotiation. In this case the subject is of type OutputStreamContent and i see no options to geht the serialized body from it.
Any idea how to log the serialized response json in my logging feature? Or maybe there is another option using the ktor server? I mean i could serialize my object manually and respond plain text, but thats an ugly way to do it.
I'm not shure about if this is the best way to do it, but here it is:
public fun ApplicationResponse.toLogString(subject: Any): String = when(subject) {
is TextContent -> subject.text
is OutputStreamContent -> {
val channel = ByteChannel(true)
runBlocking {
(subject as OutputStreamContent).writeTo(channel)
val buffer = StringBuilder()
while (!channel.isClosedForRead) {
channel.readUTF8LineTo(buffer)
}
buffer.toString()
}
}
else -> String()
}
Getting Response{protocol=http/1.1, code=404, message=Not Found, url=https://test.test.com/service/one}
The url is correct as postman works fine.
I have tried looking into this error but most things come back with URL was in correct. and the error itself is vague.
code that starts it. the builder is a json string that is valid. I have tested it in postman.
CoroutineScope(Dispatchers.Default).launch {
val call = submitService.submitCarton(builder.toString())
Log.d("submit", "begining")
withContext(Dispatchers.Main) {
if (call.isSuccessful) {
Log.d("submit",call.body() as String)
} else {
Log.d("submit", "else....")
}
}
}
service factory:
fun makeSubmitService() : SubmitService{
val url = "https://test.test.com/service/"
return Retrofit.Builder().baseUrl(url)
.client(okHttpClient).addConverterFactory(GsonConverterFactory.create())
.build().create(SubmitService::class.java)
}
interface:
interface SubmitService {
#POST("one")
suspend fun submitCarton(#Body json: String): Response<myModel>
}
Expected results are a json response however I am not getting that far.
edit: I created a okhttpclient and did a request manual and I get a message 200 ok.
code for my test
val JSON = MediaType.parse("application/json; charset=utf-8")
val client = OkHttpClient()
val body = "some json"
val requestBody = RequestBody.create(JSON, body)
val request = Request.Builder()
.url("https://test.test.com/service/one")
.post(requestBody)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(request: Request, e: IOException) {
Log.e("test", e.toString())
}
#Throws(IOException::class)
override fun onResponse(response: Response) {
Log.d("test", response.toString())
}
})
Solved it myself.
Issue was dumb, retrofit2 was giving 404 even though the web service was returning a error message.
added
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.1'
private val interceptor = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
private val okHttpClient = OkHttpClient().newBuilder()
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.build()
found out retrofit was sending a very unformatted string
"{ \"all my json filled with \" }"
instead of
{ json }
fixed it by adding
.addConverterFactory(ScalarsConverterFactory.create())
to my service factory
for anyone wondering why I am basically creating the json as a string instead of using a JSON object is because the service I talk to really really wants it to be in a very specific order which JSON just don't care about it however it wants it to look like JSON as well...
Im trying to call a service via POST that sends an e-mail to the user, the body always return empty, I have to deal with the response code.
e.g. 204 = success.
Im trying to deal this way, but im not succeeding
Service:
#POST("xxxxxx")
fun resendBankSlip(#Path("userId") userId: Int): Deferred<Response>
ViewModel:
scope.launch {
try {
_loading.value = true
val response = repository.sendEmail(userId)
if (!response.isSuccessful) {
_error.value = R.string.generic_error_message
}
} catch (e: Throwable) {
_error.value = R.string.generic_error_message
} finally {
_loading.value = false
}
}
The error happens on val response = repository.sendEmail(userId)
Exception:
java.lang.IllegalArgumentException: 'okhttp3.Response' is not a valid response body type. Did you mean ResponseBody?
for method EmailService.sendEmail
Any idea?
You probably confused okhttp3.Response with retrofit.Response. Try to use retrofit2.Response wrapper in API response like that:
#POST("xxxxxx")
fun resendBankSlip(#Path("userId") userId: Int): Deferred<retrofit2.Response<Unit>>
After that you can easily get response code via response.code().
Also note that I passed Unit as Response's type argument because you don't need the body. In other cases you should pass an actual type of response body.
I'm building a very simple application in Kotlin with Vertx and RxJava 2 (RxKotlin), using Kovert REST framework and Retrofit. I have retrofit-vertx adapter and the RxJava2 Retrofit adapter. I can return an arbitrary list from my listUndergroundStations() method, but whenever I try to load from the remote API I get the following error:
Jun 23, 2017 2:16:29 PM uk.amb85.rxweb.api.UndergroundRestController
SEVERE: HTTP CODE 500 - /api/underground/stations - java.io.IOException: java.lang.IllegalStateException: message == null
java.lang.RuntimeException: java.io.IOException: java.lang.IllegalStateException: message == null
at io.reactivex.internal.util.ExceptionHelper.wrapOrThrow(ExceptionHelper.java:45)
at io.reactivex.internal.observers.BlockingMultiObserver.blockingGet(BlockingMultiObserver.java:91)
at io.reactivex.Single.blockingGet(Single.java:2148)
at uk.amb85.rxweb.api.UndergroundRestController$listUndergroundStations$1.invoke(UndergroundRestController.kt:35)
at uk.amb85.rxweb.api.UndergroundRestController$listUndergroundStations$1.invoke(UndergroundRestController.kt:13)
at nl.komponents.kovenant.TaskPromise$wrapper$1.invoke(promises-jvm.kt:138)
at nl.komponents.kovenant.TaskPromise$wrapper$1.invoke(promises-jvm.kt:130)
at nl.komponents.kovenant.NonBlockingDispatcher$ThreadContext.run(dispatcher-jvm.kt:327)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.io.IOException: java.lang.IllegalStateException: message == null
at com.julienviet.retrofit.vertx.VertxCallFactory$VertxCall.lambda$enqueue$0(VertxCallFactory.java:90)
at io.vertx.core.impl.FutureImpl.tryFail(FutureImpl.java:170)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleException(HttpClientResponseImpl.java:270)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleEnd(HttpClientResponseImpl.java:259)
at io.vertx.core.http.impl.ClientConnection.handleResponseEnd(ClientConnection.java:361)
at io.vertx.core.http.impl.ClientHandler.doMessageReceived(ClientHandler.java:80)
at io.vertx.core.http.impl.ClientHandler.doMessageReceived(ClientHandler.java:38)
at io.vertx.core.http.impl.VertxHttpHandler.lambda$channelRead$0(VertxHttpHandler.java:71)
at io.vertx.core.impl.ContextImpl.lambda$wrapTask$2(ContextImpl.java:335)
at io.vertx.core.impl.ContextImpl.executeFromIO(ContextImpl.java:193)
at io.vertx.core.http.impl.VertxHttpHandler.channelRead(VertxHttpHandler.java:71)
at io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:122)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:435)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:293)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:267)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:250)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1228)
at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1039)
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:411)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:248)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1334)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:926)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:129)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:642)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:565)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:479)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:441)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:858)
... 1 more
Caused by: java.lang.IllegalStateException: message == null
at okhttp3.Response$Builder.build(Response.java:431)
at com.julienviet.retrofit.vertx.VertxCallFactory$VertxCall.lambda$null$1(VertxCallFactory.java:109)
at io.vertx.core.http.impl.HttpClientResponseImpl$BodyHandler.notifyHandler(HttpClientResponseImpl.java:301)
at io.vertx.core.http.impl.HttpClientResponseImpl.lambda$bodyHandler$0(HttpClientResponseImpl.java:193)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleEnd(HttpClientResponseImpl.java:257)
... 36 more
I can't for the life of me work out what is causing the IllegalStateException and have googled it to death. I don't think it's Rx related because I get the same error if I make the method return Observable<List<UndergroundLine>> or even get rid of Rx entirely and return Call<List<UndergroundLine>> (adjusting the controller accordingly). However, beyond that, I'm beating my head against a wall! Is anyone able to point out the error of my ways (besides putting a cushion under my head)?
Main Verticle:
class ApiVerticle : AbstractVerticle() {
override fun start(startFuture: Future<Void>?) {
// Initialise injection.
configureKodein()
val apiRouter = configureRouter(vertx)
vertx.createHttpServer()
.requestHandler { apiRouter.accept(it) }
.listen(8080)
}
private fun configureKodein() {
Kodein.global.addImport(Kodein.Module {
import(TflUndergroundService.module)
})
}
private fun configureRouter(vertx: Vertx): Router {
val apiMountPoint = "api"
val routerInit = fun Router.() {
bindController(UndergroundRestController(), apiMountPoint)
}
val router = Router.router(vertx) initializedBy { router ->
router.routerInit()
}
return router
}
}
TflService:
interface TflService {
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<UndergroundLine>
#GET("/Line/{lineName}/StopPoints")
fun getStationsForUndergroundLine(
#Path("lineName") lineName: String
): Observable<UndergroundStation>
#GET("/Line/{lineName}/Arrivals?stopPointId={stationNaptanId")
fun getArrivalsFor(
#Path("lineName") lineName: String,
#Path("stationNaptanId") stationNaptanId: String
) : Observable<Arrival>
}
data class UndergroundLine(val id: String, val name: String)
data class UndergroundStation(val naptanId: String, val commonName: String)
data class Arrival(
val platformName: String,
val towards: String,
val currentLocation: String,
val expectedArrival: LocalDateTime)
object TflUndergroundService {
val module = Kodein.Module {
val vertx: Vertx = Vertx.currentContext().owner()
val client: HttpClient = vertx.createHttpClient()
val jacksonMapper: ObjectMapper = ObjectMapper()
jacksonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://api.tfl.gov.uk/")
.callFactory(VertxCallFactory(client))
.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
.addConverterFactory(JacksonConverterFactory.create(jacksonMapper))
.build()
val tflService: TflService = retrofit.create(TflService::class.java)
bind<TflService>() with instance(tflService)
}
}
ApiKeySecured (Just requires "appid" to be a parameter):
class ApiKeySecured(private val routingContext: RoutingContext) : KodeinGlobalAware {
val user: String = routingContext.request().getParam("appid") ?: throw HttpErrorUnauthorized()
}
The offending REST controller (in Kovert, Promise's are executed on Vertx worker thread):
class UndergroundRestController(val undergroundService: TflService = Kodein.global.instance()) {
fun ApiKeySecured.listUndergroundStations(): Promise<List<UndergroundLine>, Exception> {
//TODO: This is blocking, fix it!??
return task {
undergroundService
.getAllUndergroundLines()
.doOnError { println(it) }
.toList()
.blockingGet()
}
}
}
build.gradle:
mainClassName = "io.vertx.core.Launcher"
def mainVerticleName = "uk.amb85.rxweb.verticles.ApiVerticle"
def configurationFile = "conf/development.json"
run {
args = ["run",
mainVerticleName,
"--launcher-class=$mainClassName",
"-conf $configurationFile"
]
}
There's an issue with retrofit-vertx you are using. OkHttp3's ResponseBuilder requires message to be not null, but VertxCallFactory doesn't set it.
It's fixed in the latest version, but as it's still in development, you have to use snapshot:
repositories {
mavenCentral()
maven {
url "https://oss.sonatype.org/content/repositories/snapshots"
}
}
dependencies {
compile 'com.julienviet:retrofit-vertx:1.0.2-SNAPSHOT'
}
Switching to snapshot dependency fixes the issue you mention in your question, but there's an issue with json mapping, which can be easily fixed by switching code from:
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<UndergroundLine>
to:
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<List<UndergroundLine>>
And updating your data classes to have default empty constructor to let Jackson instantiate using reflection:
data class UndergroundLine(var id: String = "", var name: String = "")
More on emtpy default constructor for data classes.
But it's another question related to how to parse response from API you're using to Observable and should be asked if you don't find a workaround.