Changelog 2.2 version
2.2.4
released 28th February 2023
Client
ContentNegotiation: The "charset=UTF-8" part is added for the Content-Type header
To reproduce run the following code:
val client = HttpClient(Apache) {
install(ContentNegotiation) {
json()
}
}
val response = client.post("https://httpbin.org/post") {
contentType(ContentType.Application.Json)
setBody(123)
}.bodyAsText()
println(response)
I expect a request Content-Type
header to be application/json
(like in 1.6.7) but actually it's application/json; charset=UTF-8
.
The changed behavior unnecessary breaks users' applications after migration to 2.0.0 (https://kotlinlang.slack.com/archives/C0A974TJ9/p1644027042441219).
Connect timeout is not respected when using the HttpRequestRetry plugin
Issue
If the HttpTimeout
plugin is used in parallel with the HttpRequestRetry
plugin the connectTimeoutMillis
configuration is not respected and the request enters the retry loop.
If the HttpTimeout
plugin is installed after HttpRequestRetry
the requestTimeoutMillis
configuration is not respected as well.
Reproducing the problem
Here you can find a basic example that reproduces the problem.
Platform
Noticed on JVM
OkHttp: Cancelling while writing to ByteWriteChannel when overriding WriteChannelContent causes propagation of CancellationException to a caller
Hello Ktor team :)
It has been pleasure to work with Ktor so far, but we stumbled on some issue that we are not sure how to solve, I will try to provide as much information as I can but if you need anything else from me please let me know so that perhaps we can figure something out together :)
We are trying to implement the feature where we want to cancel an outgoing asset upload to the server, this is basicly what we are trying to achieve :
class StreamAssetContent(
private val fileContentStream: okio.Source
) : OutgoingContent.WriteChannelContent() {
override suspend fun writeTo(channel: ByteWriteChannel) {
val contentBuffer = Buffer()
while (fileContentStream.read(contentBuffer, BUFFER_SIZE) != -1L) {
contentBuffer.readByteArray().let { content ->
channel.writePacket(ByteReadPacket(content))
}
}
channel.writeStringUtf8(closingArray)
channel.flush()
channel.close()
}
private companion object {
const val BUFFER_SIZE = 1024 * 8L
}
}
suspend fun cancelDuringUpload() {
val httpClient = HttpClient(engine = OkHttp.create())
val uploadJob = launch {
httpClient.post(PATH_PUBLIC_ASSETS_V3) {
contentType(ContentType.MultiPart.Mixed)
setBody(StreamAssetContent(fileSource))
}
}
delay(100.milliseconds)
uploadJob.cancel()
}
according to my knowledge this should work as it cancels the coroutine it is launched in, but it crashes the app with CancellationException, we were able to solve
by isolating the throwing of the CancellationException from the insides of the Ktor
class StreamAssetContent(
private val fileContentStream: okio.Source
) : OutgoingContent.WriteChannelContent() {
override suspend fun writeTo(channel: ByteWriteChannel) {
try {
coroutineScope {
if (!channel.isClosedForWrite && producerJob.isActive) {
channel.writeStringUtf8(openingData)
val contentBuffer = Buffer()
while (fileContentStream.read(contentBuffer, BUFFER_SIZE) != -1L) {
contentBuffer.readByteArray().let { content ->
channel.writePacket(ByteReadPacket(content))
}
}
channel.writeStringUtf8(closingArray)
channel.flush()
channel.close()
}
}
} catch (e: Exception) {
channel.flush()
channel.close()
producerJob.completeExceptionally(e)
throw IOException(e.message)
} finally {
producerJob.complete()
}
}
}
This would be a nice fix, but unfortunetly this was not enough, because when the timing is right the content of this class coming from Ktor also can throw CancellationException
internal class StreamRequestBody(
private val contentLength: Long?,
private val block: () -> ByteReadChannel`
) : RequestBody() {
override fun contentType(): MediaType? = null
override fun writeTo(sink: BufferedSink) {
block().toInputStream().source().use {
sink.writeAll(it)
}
}
override fun contentLength(): Long = contentLength ?: -1
override fun isOneShot(): Boolean = true
}
so we again override it and this is we come up with :
class ByteChannelRequestBody(
private val contentLength: Long?,
private val callContext: CoroutineContext,
private val block: () -> ByteReadChannel
) : RequestBody(), CoroutineScope {
private val producerJob = Job(callContext[Job])
override val coroutineContext: CoroutineContext
get() = callContext + producerJob + Dispatchers.IO
override fun contentLength(): Long = contentLength ?: -1
override fun contentType(): MediaType? = null
override fun writeTo(sink: BufferedSink) {
withJob(producerJob) {
if (producerJob.isActive) {
block().toInputStream().source().use {
sink.writeAll(it)
}
}
}
}
private inline fun <T> withJob(job: CompletableJob, block: () -> T): T {
try {
return block()
} catch (ex: Exception) {
job.completeExceptionally(ex)
// wrap all exceptions thrown from inside `okhttp3.RequestBody#writeTo(..)` as an IOException`
throw IOException(ex)
} finally {
job.complete()
}
}
}
the issue here is that in order to use this imlementation I had to copy-paste Ktor source code, because the place that is allowing us to use our custom Class is Ktor internal. This requires us to maintain the Ktor classes with each update, which we would ike not to do since you guys are doing it way better :P
internal fun OutgoingContent.convertToOkHttpBody(callContext: CoroutineContext): RequestBody = when (this) {
is OutgoingContent.ByteArrayContent -> bytes().let {
it.toRequestBody(null, 0, it.size)
}
is OutgoingContent.ReadChannelContent -> ByteChannelRequestBody(contentLength, callContext) { readFrom() }
is OutgoingContent.WriteChannelContent -> {
ByteChannelRequestBody(contentLength, callContext) {
GlobalScope.writer(callContext) { writeTo(channel) }.channel
}
}
is OutgoingContent.NoContent -> ByteArray(0).toRequestBody(null, 0, 0)
else -> throw UnsupportedContentTypeException(this)
}
We are on :
okio = 3.2.0
ok-http = 4.9.3
ktor = 2.2.1
Here is also the stacktrace when the issues are happening:
com.wire.kalium.network.exceptions.KaliumException$GenericError
at com.wire.kalium.network.api.v2.authenticated.AssetApiV2.uploadAsset$suspendImpl(AssetApiV2.kt:89)
at com.wire.kalium.network.api.v2.authenticated.AssetApiV2$uploadAsset$1.invokeSuspend(Unknown Source:18)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
Caused by: java.util.concurrent.CancellationException: test
at com.wire.kalium.network.http.request.ByteChannelRequestBody.writeTo(ByteChannelRequestBody.kt:63)
at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.kt:59)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:34)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:517)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
at java.lang.Thread.run(Thread.java:1012)
so my question to you, is there anything we are perhaps doing wrong, or is this simply not yet easily supported in Ktor ?
There is also an issue on this from AWS :
https://github.com/awslabs/aws-sdk-kotlin/issues/733
And this is the PR coming from us - Wire
https://github.com/wireapp/kalium/pull/1288
thanks for any help and let us know if you need anything else from us
Core
URLs with underscore fail to parse correctly in HTTP client request
Unable to use underscores in request URL with Ktor http client. Client fails to parse port and name correctly urls with underscores.
For example http://my_service:8080
fails to parse correctly. Calling URL("http://my_service:8080")
on seems to parse port and hostname but calling toURI()
on it for seems to fail to parse port and hostname correcly. This is an issue with URLBuilder.takeFrom(url: URL): URLBuilder = takeFrom(url.toURI())
This seems to be somewhat known issue with Java URI class.
Hostnames with underscores are quite common in docker environments and crucial for my use case. Is this something you would be looking to support?
Link to related method: https://github.com/ktorio/ktor/blob/0081f943b434bdd0afd82424b389629e89a89461/ktor-http/jvm/src/io/ktor/http/URLUtilsJvm.kt#L49
Generator
Create MongoDB Plugin for Generator
The Plugin should be listed in the Generator as a MongoDB ${used library}
plugin with some short description and code snippet. On apply it should add:
- required dependencies
- simple database model with initialization
- 4 CRUD endpoints
Update the latest version of Ktor by the latest version of Ktor Gradle plugin
We have a problem after releasing a new version of Ktor; there is a time gap between the release of Ktor and the Ktor Gradle plugin, so a new project with the latest released version cannot be built because of the absence of the respective version of Ktor Gradle plugin.
Create Postgres JDBC Plugin for Generator
The Plugin should be listed in the Generator as a Postgres
plugin with some short description and code snippet. On apply it should add:
- required dependencies
- simple database model with initialization
- 4 CRUD endpoints
Create Exposed Plugin for Generator
The Plugin should be listed in the Generator as an Exposed
plugin with some short description and code snippet. On apply it should add:
- expose dependencies
- simple database model with initialization
- 4 CRUD endpoints
IntelliJ IDEA Plugin
OpenApi for StatusPages is cached and not updated accordingly to the code
- Type
fun Application.configureRouting() {
install(StatusPages) {
exception<IllegalStateException> { call, cause ->
call.respond(HttpStatusCode.BadGateway, "This is response")
}
}
routing {
get("/") {
call.respondText("Hello World!")
}
get("/{name}") {
throw IllegalStateException("FOO")
}
}
}
- Generate OpenAPI
- Remove
exception
handler:
fun Application.configureRouting() {
install(StatusPages) {
}
routing {
get("/") {
call.respondText("Hello World!")
}
get("/{name}") {
throw IllegalStateException("FOO")
}
}
}
- Generate OpenAPI.
Expected: Now OpenAPI for /{name}
does not reflect response HttpStatusCode.BadGateway, "This is response"
now.
Actual: It hasn't changed.
NullPointerException in KtorRoutingVisitor.processLastArgumentsBodyIfRouteExtensionLambda
at io.ktor.ide.KtorRoutingVisitor.processLastArgumentsBodyIfRouteExtensionLambda(KtorUrlResolver.kt:149)
at io.ktor.ide.KtorRoutingVisitor.visitCallExpression(KtorUrlResolver.kt:188)
at org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression.accept(KotlinUFunctionCallExpression.kt:165)
at org.jetbrains.uast.internal.ImplementationUtilsKt.acceptList(implementationUtils.kt:14)
at org.jetbrains.uast.UBlockExpression.accept(UBlockExpression.kt:21)
at org.jetbrains.uast.ULambdaExpression.accept(ULambdaExpression.kt:40)
at org.jetbrains.uast.internal.ImplementationUtilsKt.acceptList(implementationUtils.kt:14)
at org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression.accept(KotlinUFunctionCallExpression.kt:169)
at org.jetbrains.uast.internal.ImplementationUtilsKt.acceptList(implementationUtils.kt:14)
at org.jetbrains.uast.UBlockExpression.accept(UBlockExpression.kt:21)
at org.jetbrains.uast.ULambdaExpression.accept(ULambdaExpression.kt:40)
at org.jetbrains.uast.internal.ImplementationUtilsKt.acceptList(implementationUtils.kt:14)
at org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression.accept(KotlinUFunctionCallExpression.kt:169)
at io.ktor.ide.KtorUrlResolverKt.getAllUrlMappings$lambda$3$lambda$2(KtorUrlResolver.kt:65)
at io.ktor.ide.KtorUtilsKt.withProgress(KtorUtils.kt:112)
at io.ktor.ide.KtorUrlResolverKt.getAllUrlMappings$lambda$3(KtorUrlResolver.kt:64)
at com.intellij.psi.impl.PsiCachedValueImpl.doCompute(PsiCachedValueImpl.java:39)
at com.intellij.util.CachedValueBase.lambda$getValueWithLock$3(CachedValueBase.java:231)
at com.intellij.util.CachedValueBase.computeData(CachedValueBase.java:41)
at com.intellij.util.CachedValueBase.lambda$getValueWithLock$4(CachedValueBase.java:231)
at com.intellij.openapi.util.RecursionManager$1.computePreventingRecursion(RecursionManager.java:112)
at com.intellij.openapi.util.RecursionGuard.doPreventingRecursion(RecursionGuard.java:42)
at com.intellij.openapi.util.RecursionManager.doPreventingRecursion(RecursionManager.java:66)
at com.intellij.util.CachedValueBase.getValueWithLock(CachedValueBase.java:232)
at com.intellij.psi.impl.PsiCachedValueImpl.getValue(PsiCachedValueImpl.java:28)
at com.intellij.util.CachedValuesManagerImpl.getCachedValue(CachedValuesManagerImpl.java:72)
at com.intellij.psi.util.CachedValuesManager.getCachedValue(CachedValuesManager.java:111)
at io.ktor.ide.KtorUrlResolverKt.getAllUrlMappings(KtorUrlResolver.kt:61)
at io.ktor.ide.KtorUrlResolverKt.access$getAllUrlMappings(KtorUrlResolver.kt:1)
at io.ktor.ide.KtorUrlResolver$getVariants$2.invoke(KtorUrlResolver.kt:41)
at io.ktor.ide.KtorUrlResolver$getVariants$2.invoke(KtorUrlResolver.kt:41)
at kotlin.sequences.FlatteningSequence$iterator$1.ensureItemIterator(Sequences.kt:315)
at kotlin.sequences.FlatteningSequence$iterator$1.hasNext(Sequences.kt:303)
at kotlin.sequences.TransformingSequence$iterator$1.hasNext(Sequences.kt:214)
at kotlin.sequences.TransformingSequence$iterator$1.hasNext(Sequences.kt:214)
at kotlin.sequences.TransformingSequence$iterator$1.hasNext(Sequences.kt:214)
at kotlin.sequences.FilteringSequence$iterator$1.calcNext(Sequences.kt:169)
at kotlin.sequences.FilteringSequence$iterator$1.hasNext(Sequences.kt:194)
at com.intellij.microservices.gotosymbol.UrlSearchEverywhereContributor$fetchWeightedElements$lambda$2$$inlined$computeInReadActionWithWriteActionPriority$1.run(UrlSearchEverywhereContributor.kt:205)
at com.intellij.openapi.application.impl.ApplicationImpl.tryRunReadAction(ApplicationImpl.java:1086)
at com.intellij.openapi.progress.util.ProgressIndicatorUtils.lambda$runInReadActionWithWriteActionPriority$0(ProgressIndicatorUtils.java:71)
at com.intellij.openapi.progress.util.ProgressIndicatorUtilService.runActionAndCancelBeforeWrite(ProgressIndicatorUtilService.java:63)
at com.intellij.openapi.progress.util.ProgressIndicatorUtils.runActionAndCancelBeforeWrite(ProgressIndicatorUtils.java:128)
at com.intellij.openapi.progress.util.ProgressIndicatorUtils.lambda$runWithWriteActionPriority$1(ProgressIndicatorUtils.java:109)
at com.intellij.openapi.progress.ProgressManager.lambda$runProcess$0(ProgressManager.java:68)
at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$runProcess$2(CoreProgressManager.java:188)
at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$executeProcessUnderProgress$13(CoreProgressManager.java:589)
at com.intellij.openapi.progress.impl.CoreProgressManager.registerIndicatorAndRun(CoreProgressManager.java:664)
at com.intellij.openapi.progress.impl.CoreProgressManager.computeUnderProgress(CoreProgressManager.java:620)
at com.intellij.openapi.progress.impl.CoreProgressManager.executeProcessUnderProgress(CoreProgressManager.java:588)
at com.intellij.openapi.progress.impl.ProgressManagerImpl.executeProcessUnderProgress(ProgressManagerImpl.java:60)
at com.intellij.openapi.progress.impl.CoreProgressManager.runProcess(CoreProgressManager.java:175)
at com.intellij.openapi.progress.ProgressManager.runProcess(ProgressManager.java:68)
at com.intellij.openapi.progress.util.ProgressIndicatorUtils.runWithWriteActionPriority(ProgressIndicatorUtils.java:106)
at com.intellij.openapi.progress.util.ProgressIndicatorUtils.runInReadActionWithWriteActionPriority(ProgressIndicatorUtils.java:71)
at com.intellij.microservices.gotosymbol.UrlSearchEverywhereContributor.p(UrlSearchEverywhereContributor.kt:202)
at com.intellij.concurrency.JobLauncherImpl.lambda$processImmediatelyIfTooFew$2(JobLauncherImpl.java:137)
at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$executeProcessUnderProgress$13(CoreProgressManager.java:589)
at com.intellij.openapi.progress.impl.CoreProgressManager.registerIndicatorAndRun(CoreProgressManager.java:664)
at com.intellij.openapi.progress.impl.CoreProgressManager.computeUnderProgress(CoreProgressManager.java:620)
at com.intellij.openapi.progress.impl.CoreProgressManager.executeProcessUnderProgress(CoreProgressManager.java:588)
at com.intellij.openapi.progress.impl.ProgressManagerImpl.executeProcessUnderProgress(ProgressManagerImpl.java:60)
at com.intellij.concurrency.JobLauncherImpl.lambda$processImmediatelyIfTooFew$3(JobLauncherImpl.java:133)
at com.intellij.concurrency.JobLauncherImpl.processImmediatelyIfTooFew(JobLauncherImpl.java:147)
at com.intellij.concurrency.JobLauncherImpl.invokeConcurrentlyUnderProgress(JobLauncherImpl.java:44)
OpenAPI for StatusPages: java.lang.IllegalStateException: Calling invokeAndWait from read-action leads to possible deadlock
232.1708
fun Application.configureRouting() {
install(StatusPages) {
exception<IllegalStateException> { call, cause ->
call.respond(HttpStatusCode.BadGateway, "500")
}
}
routing {
get("/") {
call.respondText("Hello World!")
}
get("/{name}") {
throw IllegalStateException("FOO")
// call.respond("OK")
// val name = call.parameters["name"] ?:
// call.respondText("Hello $name!")
}
}
}
Exception:
java.lang.IllegalStateException: Calling invokeAndWait from read-action leads to possible deadlock.
at com.intellij.openapi.application.impl.ApplicationImpl.invokeAndWait(ApplicationImpl.java:454)
at com.intellij.openapi.application.ex.ApplicationUtil.invokeAndWaitSomewhere(ApplicationUtil.java:145)
at com.intellij.openapi.progress.impl.CoreProgressManager.runProcessWithProgressSynchronously(CoreProgressManager.java:532)
at com.intellij.openapi.progress.impl.ProgressManagerImpl.runProcessWithProgressSynchronously(ProgressManagerImpl.java:85)
at com.intellij.openapi.progress.impl.CoreProgressManager.run(CoreProgressManager.java:367)
at com.intellij.openapi.progress.ProgressManager.run(ProgressManager.java:208)
at io.ktor.ide.actions.test.TestActionsUtils$Companion.executeWithModalProgress(TestActionsUtils.kt:65)
at io.ktor.ide.actions.test.TestActionsUtils$Companion.findParentRoutingCall$intellij_ktor_starter(TestActionsUtils.kt:213)
at io.ktor.ide.actions.test.TestActionsUtils$Companion.findKtorApplicationConfig(TestActionsUtils.kt:116)
at io.ktor.ide.oas.KtorStatusPagesOasSupportKt.findStatusPagesMapping(KtorStatusPagesOasSupport.kt:22)
at io.ktor.ide.oas.KtorOasVisitor.findStatusPagesMappingCached$lambda$6(KtorOasVisitor.kt:120)
Ktor IDE does not generate proper OpenAPI with StatusPages and recursive calls (plus `error(...)` calls)
Ex:
install(StatusPages) {
exception<Throwable> { call, cause ->
call.respondText(text = "500: ${'$'}cause", status = HttpStatusCode.InternalServerError)
}
exception<IllegalStateException> { call, cause ->
call.respond("AAA")
}
}
...
get("/eee") {
error("aaa")
myError()
}
...
fun myError(): Nothing = throw Exception()
In this case, /eee
route in OpenAPI does not represent handlers from StatusPages because myError
is recursive call that throws (although it is not Route. or Call. extension)
Samples
Integration tests for API secured with (RSA signed) JSON web tokens sample
Apply Ktor Gradle plugin to all sample projects and remove specific versions for artifacts
If the Ktor Gradle plugin is applied to the project, the specific versions for Ktor's dependencies are redundant.
Server
Routing: Wrong content-type results in 405 instead of 415 status code with two routes
In continuation of KTOR-4849. The example provided there is indeed fixed in 2.2.3. However, if we add another route, another issue emerges. Instead of 415 I get 405.
package com.example
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.log
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.response.respond
import io.ktor.server.routing.accept
import io.ktor.server.routing.contentType
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
fun main() {
embeddedServer(Netty, port = 3000) {
module()
}.apply {
start(wait = true)
}
}
fun Application.module(): Unit {
routing {
route("/some/plain/path", HttpMethod.Post) {
contentType(ContentType.Application.Json) {
handle {
call.respond("ok")
}
}
}
route("/some/plain/path", HttpMethod.Get) {
contentType(ContentType.Application.Json) {
handle {
call.respond("ok")
}
}
}
}
}
$ curl -i -XPOST localhost:3000/some/plain/path -H'Content-Type: application/xml'
HTTP/1.1 405 Method Not Allowed
Content-Length: 0
Compressing the response will result in unexpected ERROR log output after processing in the StatusPages
sample code
/compress
returns a compressed response./normal
returns a not compressed response.
fun Application.module() {
val logger = LoggerFactory.getLogger(Application::class.java)
install(Compression) {
condition {
request.uri.startsWith("/compress")
}
}
install(StatusPages) {
exception<IllegalStateException> { call, cause ->
logger.info("error! request uri: {}", call.request.uri)
call.respond(HttpStatusCode.BadRequest, "400error!!!!")
}
}
routing {
get("/compress") { throw IllegalStateException() }
get("/normal") { throw IllegalStateException() }
}
}
result(log output)
Compressing the response produces an unexpected ERROR log.
2023-02-01 18:10:33.526 [eventLoopGroupProxy-4-1] INFO i.k.server.application.Application - error! request uri: /normal
2023-02-01 18:10:41.711 [eventLoopGroupProxy-4-1] INFO i.k.server.application.Application - error! request uri: /compress
2023-02-01 18:10:41.736 [eventLoopGroupProxy-4-1] ERROR ktor.application - 400 Bad Request: GET - /compress
Cause
Could the following code be the cause? The response is not changed to sent.
The exception is resent because the response has not been sent.
https://github.com/ktorio/ktor/blob/f97c1a3432041471c1ae3eed2ecc9047c8ad44a7/ktor-server/ktor-server-core/jvmAndNix/src/io/ktor/server/application/hooks/CommonHooks.kt#L40-L49
Javadoc for Resources.kt cannot be compiled
The Javadoc for Resources.kt shows the following example, which is wrong (the Users data class must have at least one primary constructor parameter). So the best fix would probably be to switch out the data classes with regular classes.
I wouldn't mind opening a pull request btw.
/**
* Adds support for type-safe routing using [ResourcesCore].
*
* Example:
* ```kotlin
* @Serializable
* @Resource("/users")
* data class Users {
* @Serializable
* @Resource("/{id}")
* data class ById(val parent: Users = Users(), val id: Long)
*
* @Serializable
* @Resource("/add")
* data class Add(val parent: Users = Users(), val name: String)
* }
Shared
kotlinx.serialization.SerializationException is lost for the classes that have generic type parameters
Consider this class:
@Serializable
class Foo<T>(val f: Float, val t: T?)
And this test that uses kotlinx.serialization.json.Json { allowSpecialFloatingPointValues = false }
(other test infrastructure taken from Ktor's JsonClientKotlinxSerializationTest
):
@Test fun testGenericFloat(): Unit = testWithEngine(CIO) {
configureClient()
test { client ->
val result = client.post {
url(path = "/echo", port = serverPort)
contentType(defaultContentType)
setBody(Foo<String>(Float.NEGATIVE_INFINITY, "f"))
}.body<Foo<String>>()
}
}
The problem is following: normally, Json.encodeToString
can throw arbitrary SerializationException
to indicate problems with input data - e.g. invalid floating point value. It is not the only exception we can throw, but it is very simple to be illustrative.
However, Ktor apparently swallows this exception in this point: https://github.com/ktorio/ktor/blob/bad1c2da472646318a402342a249cc6300bc0940/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/common/src/io/ktor/serialization/kotlinx/KotlinxSerializationBase.kt#L29
There are several problems with code:
- Ktor assumes that only
SerializationException
we can get is 'serializer not found', but it is not true. - Exception message or stack trace is lost.
- Instead, we proceed to do
guessSerializer
(https://github.com/ktorio/ktor/blob/bad1c2da472646318a402342a249cc6300bc0940/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/common/src/io/ktor/serialization/kotlinx/KotlinxSerializationBase.kt#L34) which don't support generic classes. As a result, there is a cryptic message after running the test:Serializer for class 'Foo' is not found. Mark the class as @Serializable or provide the serializer explicitly.
, while it is clear thatFoo
is@Serializable
.
Given that mentioned https://github.com/Kotlin/kotlinx.serialization/issues/1163 is already fixed, I suggest rewriting this place
2.2.3
released 1st February 2023
Client
ContentNegotiation: "Skipping because the type is ignored" log message is unclear
What would it mean
i.k.s.p.c.ContentNegotiation - Skipping because the type is ignored.
This message is unclear
IU-223.8214.52, JRE 17.0.5+1-b653.23x64 JetBrains s.r.o., OS Windows 10(amd64) v10.0 , screens 1920.0x1080.0, 1920.0x1080.0
FileStorage throws java.io.FileNotFoundException (File name too long) when request path is long
REQUEST https://api.github.com/repos/Example/Repo/compare/eap...GL-1412-folder-with-multi-repos-in-it-the-you-are-offline-footer-stays-+-doesnt failed with exception: java.io.FileNotFoundException: /Users/username/Library/Caches/AppName/68747470733a2f2f6170692e6769746875622e636f6d2f7265706f732f4769744c6976654170702f636f72652f636f6d706172652f6561702e2e2e474c2d313431322d666f6c6465722d776974682d6d756c74692d7265706f732d696e2d69742d7468652d796f752d6172652d6f66666c696e652d666f6f7465722d73746179732d2b2d646f65736e74 (File name too long)
Environment
- Kotlin version: 1.8
- Library version: 1.4.1
- Kotlin platforms: JVM
- Gradle version: 7.4.1
- OS Version: macOS 12.5
HttpRequestRetry retries on FileNotFoundException thrown by FileStorage
With a configuration that includes HttpCache and HttpRequestRetry plugins such as:
HttpClient {
expectSuccess = true
install(HttpCache) { privateStorage(FileStorage(Files.createDirectories(Paths.get(dirs.cacheDir)).toFile())) }
install(ContentNegotiation) { json(json) }
Logging {
level = LogLevel.INFO
}
install(HttpRequestRetry) {
maxRetries = 5
}
Exceptions thrown by HttpCache such as FileNotFoundException (see KTOR-5443) cause HttpRequestRetry to retry the request and hit the cache again. I would expect the behavior to be that any exceptions thrown accessing the cache do not fail the request but instead continue the request as if there was a cache miss.
Add Client Plugins Trace Logging
Docs
Remove Ktor samples that duplicate existing samples in docs
- Remove from https://ktor.io/learn/
- Redeploy the site
- Update TeamCity configuration
- Remove from the repo: https://github.com/ktorio/ktor-samples
Generator
Cannot compile a project generated with the Exposed plugin
To reproduce, generate a project with the Exposed plugin. As a result, the build fails with the Overload resolution ambiguity
error:
public final val h2_version: String defined in Build_gradle
public final val h2_version: String defined in Build_gradle
Migration breaks Gson import
To reproduce, create a new project with Ktor 2.1.3 through the wizard with the Gson
plugin and run "Migrate Project to Latest Ktor Version...".
As a result, the correct import statement import io.ktor.serialization.gson.*
is replaced with the incorrect one import io.ktor.serialization.kotlinx.json.gson.*
in the plugins/Serialization.kt
file.
IntelliJ IDEA Plugin
`parent` params from nested resources are erroneously represented in OpenAPI
Example:
@Resource("/v1")
class V1Api {
@Resource("/reports")
class Reports(val parent: V1Api) {
@Resource("/")
class New(val parent: Reports)
@Resource("{id}")
class Id(val parent: Reports, val id: Int) {
@Resource("/timeline")
class Timeline(val parent: Id)
}
}
}
routing {
post<V1Api.Reports.New> {
call.respondText("hello")
}
}
Resulting schema contains:
post:
description: ""
parameters:
- name: "parent"
in: "query"
required: true
schema:
type: "object"
- name: "parent"
in: "query"
required: true
schema:
type: "object"
While parent
fields should not be considered as query parameters, but as a "technical" references to the parent class in the code (see https://ktor.io/docs/type-safe-routing.html#resource_nested)
Server
Multipart File doesn't upload whole file, throws "Unexpected EOF: expected 4096 more bytes" for larger files
Steps to reproduce:
- check out the reproducer project. It contains a simple upload sample and Gradle
run
task that constrains memory to 128mb. - Create a large file to upload (e.g. on macOS with
mkfile -n 2g upload.bin
) - Navigate to
http://0.0.0.0:8080/upload
and upload the file
The significant part seems to be UploadRoute
's stream-copying:
it.streamProvider().use { partstream ->
targetFile.outputStream().use { os ->
partstream.copyTo(os, 8192)
}
}
It results in the following:
2021-11-21 20:49:35.681 [eventLoopGroupProxy-4-1] ERROR ktor.application - Unhandled exception caught for CoroutineName(call-handler)
kotlinx.coroutines.channels.ClosedReceiveChannelException: Unexpected EOF: expected 4096 more bytes
at io.ktor.utils.io.ByteBufferChannel.readFullySuspend(ByteBufferChannel.kt:573)
at io.ktor.utils.io.ByteBufferChannel.readFully(ByteBufferChannel.kt:565)
at io.ktor.utils.io.ByteBufferChannel.readPacketSuspend(ByteBufferChannel.kt:781)
at io.ktor.utils.io.ByteBufferChannel.readPacket$suspendImpl(ByteBufferChannel.kt:767)
at io.ktor.utils.io.ByteBufferChannel.readPacket(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteReadChannelKt.readPacket(ByteReadChannel.kt:198)
at io.ktor.http.cio.MultipartKt$parseMultipart$1.invokeSuspend(Multipart.kt:342)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.internal.DispatchedContinuation.resumeWith(DispatchedContinuation.kt:205)
at io.ktor.utils.io.internal.CancellableReusableContinuation.resumeWith(CancellableReusableContinuation.kt:93)
at io.ktor.utils.io.ByteBufferChannel.resumeWriteOp(ByteBufferChannel.kt:2138)
at io.ktor.utils.io.ByteBufferChannel.bytesRead(ByteBufferChannel.kt:887)
at io.ktor.utils.io.ByteBufferChannel.readAsMuchAsPossible(ByteBufferChannel.kt:483)
at io.ktor.utils.io.ByteBufferChannel.readAvailable$suspendImpl(ByteBufferChannel.kt:678)
at io.ktor.utils.io.ByteBufferChannel.readAvailable(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteBufferChannel.readAvailableSuspend(ByteBufferChannel.kt:722)
at io.ktor.utils.io.ByteBufferChannel.access$readAvailableSuspend(ByteBufferChannel.kt:24)
at io.ktor.utils.io.ByteBufferChannel$readAvailableSuspend$2.invokeSuspend(ByteBufferChannel.kt)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:469)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:500)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.ktor.server.netty.EventLoopGroupProxy$Companion.create$lambda-1$lambda-0(NettyApplicationEngine.kt:260)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:831)
My two-gigabyte sample files have only arrived in part to /tmp
, as well.
DropwizardMetricsPlugin logs status code incorrectly when is used together with StatusPages plugin
Event Routing.RoutingCallFinished
happens before hook CallFailed
.
Because of this all the responses that were handled by the StatusPages plugin has incorrect metrics status 0.
DropwizardMetrics.kt
on(MonitoringEvent(Routing.RoutingCallFinished)) { call ->
val routingMetrics = call.attributes.take(routingMetricsKey)
val status = call.response.status()?.value ?: 0
val statusMeter =
pluginConfig.registry.meter(name(pluginConfig.baseName, routingMetrics.name, status.toString()))
statusMeter.mark()
routingMetrics.context.stop()
}
StatusPages.kt
on(CallFailed) { call, cause ->
if (call.attributes.contains(statusPageMarker)) return@on
LOGGER.trace("Call ${call.request.uri} failed with cause $cause")
val handler = findHandlerByValue(cause)
if (handler == null) {
LOGGER.trace("No handler found for exception: $cause for call ${call.request.uri}")
throw cause
}
call.attributes.put(statusPageMarker, Unit)
call.application.mdcProvider.withMDCBlock(call) {
LOGGER.trace("Executing $handler for exception $cause for call ${call.request.uri}")
handler(call, cause)
}
}
In the documentation there is an example that uses ResponseSent hook which seems to me more suitable for such case https://ktor.io/docs/events.html#custom-events
Is there any workaround, or should the DropwizardMetricsPlugin be modified to subscribe to another hook?
I also saw a discussion here to back up this idea https://github.com/ktorio/ktor/pull/2792#discussion_r791846131 however this branch wasn't merged to main branch afaics.
Log HTTP request time
I installed CallLogging on my Ktor server and it works great to keep track of what's going on!
I was wondering if there is a way to also log the ms it took to process the request, as of now it looks like this:
2020-11-05 14:12:04.248 [ktor-jetty-8080-5] INFO Application - 200 OK: GET - /users/23
Netty: Unable to set the `tcpKeepAlive`
Hi, team!
Problem:
I'm trying to set tcp keep-alive settings for our ktor netty server:
embeddedServer(Netty, port = Configuration.appPort, configure = {
tcpKeepAlive = true
...
}
...
)
However, netty warns me that SO_KEEPALIVE
is an unknown option: {"timestamp":"2022-12-21 17:27:23.982","level":"WARN","thread":"main","logger":"io.netty.bootstrap.ServerBootstrap","message":"Unknown channel option 'SO_KEEPALIVE' for channel '[id: 0x0bdc9e8b]'","context":"default"}
Workaround:
The problem could be solved with this workaround:
embeddedServer(Netty, port = Configuration.appPort, configure = {
configureBootstrap = {
childOption(ChannelOption.SO_KEEPALIVE, true)
}
...
}
...
)
Possible fix:
So, it looks like, you are setting SO_KEEPALIVE
option in a wrong way:
if (configuration.tcpKeepAlive) {
option(NioChannelOption.SO_KEEPALIVE, true)
}
Details:
implementation("io.ktor:ktor-server-netty:2.0.1")
implementation("io.netty:netty-transport-native-epoll:4.1.77.Final:linux-x86_64")
implementation("io.netty:netty-transport-native-kqueue:4.1.77.Final:osx-x86_64")
HOCON: CLI parameters don't override custom array properties since 2.1.0
Even after fixing https://youtrack.jetbrains.com/issue/KTOR-5000 CLI parameters not overriding custom array properties.
application.conf
example:
ktor {
deployment {
port = 8085
}
}
some {
custom {
arrProp = [
pumpurum,
// ololo,
]
}
}
Command example:
java -jar build/libs/app.jar -P:some.custom.arrProp.0=azaza -P:some.custom.arrProp.1=ololo -P:some.custom.arrProp.2=ururu
Expected bevaviour:
val n = config.config("some.custom").property("arrProp").getList()
// n is ["azaza", "ololo", "ururu"] (from command line)
Actual behaviour:
val n = config.config("some.custom").property("arrProp").getList()
// n is ["pumpurum"] (default value from file)
Other
Support `call.receive` (request body) in OpenAPI generator
Example. When user calls
val report = call.receive<Report>()
There should be the following entry in OpenAPI:
requestBody:
content:
'*/*':
schema:
$ref: "#/components/schemas/Report"
required: true
Make OAuth2 functionality multiplatform
Summary
OAuth2 functionality is currently JVM-only code under ktor-features/ktor-auth/
.
But I'm wondering if this should be a common client feature and be ported to ktor-client-features/ktor-client-auth/
Environment
Ktor version: 1.4.1
Subsystem: Client
Engine: OkHttp
Details
Perhaps it is my lack of understanding or perhaps it's already a tech-debt item, but I don't think there is anything JVM-specific in the implementatoin of the OAuth2 authorization flows in OAuth2.kt
.
On Android/JVM, I successfully used the verifyWithOAuth2
method to retrieve an access token. However, my ultimate goal is doing this on Windows / mingw64.
OAuth2 functionality currently is JVM-only code under
ktor-features/ktor-auth/jvm/src/io/ktor/auth
However, this seems like it should be a common client feature and therefore be ported to
ktor-client-features/ktor-client-auth/common
2.2.2
released 4th January 2023
Client
HttpRequestRetry: Memory leak of coroutines objects when using the plugin
We are experiencing a memory leak in our Ktor application that is tied to the HttpRequestRetry Plugin. We have an endpoint which calls a service using an HttpClient with the CIO engine (I also switch to the Java engine and saw the same behavior) when hit. On our processes that receive higher levels of traffic we saw the heap grow excessively over time without any ability to recover the heap space. After extensive testing and many many heap dumps, I realized that there were objects tied to the HttpRequestRetry Plugin. Once, I removed the plugin, all issues went away.
The objects that I am seeing in the heap are seen in the screen shot below. The kotlinx.coroutines.InvokeOnCompletion, kotlinx.coroutines.NodeList, kotlinx.coroutines.ChildHandleNode and other coroutine objects become the top objects in the heap by size. When I remove the plugin, these objects are no longer a major presence in the heap.
{width=70%}
Our configuration of the plugin is basic
HttpClient(CIO) {
install(ContentNegotiation) {
json()
}
install(Logging) {
level = LogLevel.valueOf(System.getenv("HTTP_CLIENT_LOGGING_LEVEL"))
}
defaultRequest {
headers {
ABSTracing.id?.let { append(Constants.HEADERS.ABS_TRACING_ID_HEADER, it) }
}
}
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
retryOnException(maxRetries = 3)
exponentialDelay()
modifyRequest {
request.headers["x-retry-count"] = retryCount.toString()
}
}
}
}
iOS unit test deadlocks with DarwinClientEngine
Gzip encoding: IllegalStateException: Expected 112, actual 113
This is a follow up from https://youtrack.jetbrains.com/issue/KTOR-4653.
Commit 4e4d0e11 introduced checks that response.body.length == response.headers["content-length"]
in the non-gzip case:
val contentLength = response.contentLength()
val contentEncoding = response.headers[HttpHeaders.ContentEncoding]
if (contentEncoding == null && contentLength != null && contentLength > 0) {
check(bytes.size == contentLength.toInt()) { "Expected $contentLength, actual ${bytes.size}" }
}
In a browser context though, the "content-encoding"
header is not exposed to JS for security reasons. From MDN docs:
Only the CORS-safelisted response headers are exposed by default. For clients to be able to access other headers,
the server must list them using the Access-Control-Expose-Headers header.
Relying on content-encoding
is a risky assumption to make and breaks in some cases. For an example, the below throws:
val client = io.ktor.client.HttpClient(Js)
val response = client.get("https://raw.githubusercontent.com/apollographql/apollo-kotlin/main/renovate.json")
// IllegalStateException: Expected 112, actual 113
println(response.body<ByteArray>())
I'm not sure what the best fix is there. Maybe just removing the check completely? Given it's not done for String
conversions, is there a specific reason to do it for ByteArray
?
Core
Allow specifying immutable in CacheControl
The CacheControl
class does not allow for the full range of valid options. For example the immutable
option which is useful for static resources.
Docs
Document how to generate OpenAPI specification for EngineMain servers
Use more clear term instead of 'authorization scope' in auth docs
https://ktor.io/docs/authentication.html#authenticate-route
for example, Protect specific routes
IntelliJ IDEA Plugin
Open API: Support request headers in OpenAPI generator
example:
get("/cards") {
call.request.header("id")
call.respond(HttpStatusCode.OK, "Your card is found")
}
In OpenAPI there should be the information about request header "id"
OpenAPI: support request cookies in documentation
example:
get("/cards") {
call.request.cookie("id")?.toInt()
call.respond(HttpStatusCode.OK, "Your card is found")
}
In OpenAPI there should be the information about request cookie "id" of type integer
Open API: Support a list of values as request header
I want request header with a value as a list of strings:
get("/report/{reportType}") {
val reportType = call.parameters["reportType"]
val repeatedAccountsId: List<String>? = call.request.headers.getAll("Authorisation") // Multiple values
call.respondText("This report is for $repeatedAccountsId and it contains type $reportType!")
}
I expect that Open API documentation will be like this:
/report/{reportType}:
get:
description: ""
parameters:
- name: "reportType"
in: "path"
required: true
schema:
type: "string"
- name: "repeatedAccountsId"
in: "header"
required: false
schema:
type: array
items: {
type: string
}
But I've got Open API documentation as for header with a String value:
/report/{reportType}:
get:
description: ""
parameters:
- name: "reportType"
in: "path"
required: true
schema:
type: "string"
- name: "Authorisation"
in: "header"
required: false
schema:
type: "string"
Ktor templates use translated string for declaring the dependency when using the Language Pack
Ktor install Authentication templates popup this: {width=339px}
and choose JWT or some other needed a new gradle dependency, will cause a error:
{width=70%}
the keyword 'implementation' has been translated, which is should not translated.
Server
Engine shutdown grace period and timeout are not configurable
In each of the EngineMain implementations, a shutdown hook is added with a default grace period and shutdown timeout applied. See:
- https://github.com/ktorio/ktor/blob/main/ktor-server/ktor-server-netty/jvm/src/io/ktor/server/netty/EngineMain.kt#L24
- https://github.com/ktorio/ktor/blob/0081f943b434bdd0afd82424b389629e89a89461/ktor-server/ktor-server-jetty/jvm/src/io/ktor/server/jetty/EngineMain.kt#L24
- https://github.com/ktorio/ktor/blob/0081f943b434bdd0afd82424b389629e89a89461/ktor-server/ktor-server-tomcat/jvm/src/io/ktor/server/tomcat/EngineMain.kt#L26
- https://github.com/ktorio/ktor/blob/be1e701bf693f2bb958fecb6bb29320384168a92/ktor-server/ktor-server-cio/jvmAndNix/src/io/ktor/server/cio/EngineMain.kt#L23
I need these to be configurable, so I can tune the grace period and timeout durations for my application. Because it is not configurable, I have to duplicate the EngineMain block in my application code, which isn't ideal. Specifically, I would love to be able to specify this in my application.conf file via something like ktor.shutdown.gracePeriodMs.
This is a change I would be open to contributing, but I would need guidance on what things I should change to accomplish this.
Server cannot be started with the Swagger plugin
Create a Ktor server, include Swagger plugin (version 2.2.1) and link it as provided in documentation, e.g.:
swaggerUI(path = "api", swaggerFile = "openapi/openapi.yml")
This works correctly when run in IntelliJ. Once i bundle the app into JAR using Shadow, server is unable to start.
java -server -jar app.jar
Exception in thread "main" java.lang.IllegalArgumentException: URI is not hierarchical
at java.base/java.io.File.<init>(File.java:420)
at io.ktor.server.plugins.swagger.SwaggerKt.swaggerUI(Swagger.kt:29)
at io.ktor.server.plugins.swagger.SwaggerKt.swaggerUI$default(Swagger.kt:22)
The swaggerUI method is too restrictive and cannot be called inside a route
Right now the swaggerUI
method is the extension for the Routing
class which prevents calling it inside a route, for example, to protect it with authentication.
Netty, HSTS: UnsupportedOperationException is thrown when the server responds before HSTS plugin
I installed the HSTS Plugin to my KTOR app and always, when a static file is requested, i get this Exception:
2022-12-05 15:41:40.202 [eventLoopGroupProxy-4-3] INFO ktor.application - 200 OK: GET - /static/bootstrap/dist/css/bootstrap.min.css.map
2022-12-05 15:41:40.205 [eventLoopGroupProxy-4-3] ERROR ktor.application - 200 OK: GET - /static/bootstrap/dist/css/bootstrap.min.css.map
java.lang.UnsupportedOperationException: Headers can no longer be set because response was already completed
at io.ktor.server.netty.http1.NettyHttp1ApplicationResponse$headers$1.engineAppendHeader(NettyHttp1ApplicationResponse.kt:42)
at io.ktor.server.response.ResponseHeaders.append(ResponseHeaders.kt:57)
at io.ktor.server.response.ResponseHeaders.append$default(ResponseHeaders.kt:48)
at io.ktor.server.response.ApplicationResponsePropertiesKt.header(ApplicationResponseProperties.kt:14)
at io.ktor.server.plugins.hsts.HSTSKt$HSTS$2$1.invokeSuspend(HSTS.kt:109)
at io.ktor.server.plugins.hsts.HSTSKt$HSTS$2$1.invoke(HSTS.kt)
at io.ktor.server.plugins.hsts.HSTSKt$HSTS$2$1.invoke(HSTS.kt)
at io.ktor.server.application.PluginBuilder$onCall$2.invokeSuspend(PluginBuilder.kt:90)
at io.ktor.server.application.PluginBuilder$onCall$2.invoke(PluginBuilder.kt)
at io.ktor.server.application.PluginBuilder$onCall$2.invoke(PluginBuilder.kt)
at io.ktor.server.application.PluginBuilder$onDefaultPhase$1.invokeSuspend(PluginBuilder.kt:217)
at io.ktor.server.application.PluginBuilder$onDefaultPhase$1.invoke(PluginBuilder.kt)
at io.ktor.server.application.PluginBuilder$onDefaultPhase$1.invoke(PluginBuilder.kt)
at io.ktor.server.application.PluginBuilder$onDefaultPhaseWithMessage$1$1$1.invokeSuspend(PluginBuilder.kt:200)
at io.ktor.server.application.PluginBuilder$onDefaultPhaseWithMessage$1$1$1.invoke(PluginBuilder.kt)
at io.ktor.server.application.PluginBuilder$onDefaultPhaseWithMessage$1$1$1.invoke(PluginBuilder.kt)
at io.ktor.util.debug.ContextUtilsKt$addToContextInDebugMode$2.invokeSuspend(ContextUtils.kt:33)
at io.ktor.util.debug.ContextUtilsKt$addToContextInDebugMode$2.invoke(ContextUtils.kt)
at io.ktor.util.debug.ContextUtilsKt$addToContextInDebugMode$2.invoke(ContextUtils.kt)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:169)
at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
at io.ktor.util.debug.ContextUtilsKt.addToContextInDebugMode(ContextUtils.kt:33)
at io.ktor.server.application.PluginBuilder$onDefaultPhaseWithMessage$1$1.invokeSuspend(PluginBuilder.kt:196)
at io.ktor.server.application.PluginBuilder$onDefaultPhaseWithMessage$1$1.invoke(PluginBuilder.kt)
at io.ktor.server.application.PluginBuilder$onDefaultPhaseWithMessage$1$1.invoke(PluginBuilder.kt)
at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:14)
at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:62)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
at kotlinx.coroutines.UndispatchedCoroutine.afterResume(CoroutineContext.kt:233)
at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:102)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:138)
at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:112)
at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:14)
at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:62)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:138)
at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:112)
at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:14)
at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:62)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at io.netty.util.concurrent.AbstractEventExecutor.runTask$$$capture(AbstractEventExecutor.java:174)
at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java)
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.java:167)
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java)
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:503)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.ktor.server.netty.EventLoopGroupProxy$Companion.create$lambda-1$lambda-0(NettyApplicationEngine.kt:288)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:829)
It seems that the reason for that is the usage of onCall
here. I tested it with onCallRespond
instead and it seemed to work without throwing those Exceptions. Hope, someone can fix this.
Shared
Resource annotation should be MetaSerializable
Other
Regression in 2.2.1: Got EOF but at least 0 bytes were expected
I tried to update ktor from 2.1.3 to 2.2.1 in the IJ Platform, but I get the following error:
Exception in thread "main" java.io.EOFException: Got EOF but at least 0 bytes were expected
at io.ktor.utils.io.ByteBufferChannel.read$suspendImpl(ByteBufferChannel.kt:1658)
at io.ktor.utils.io.ByteBufferChannel.read(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteBufferChannel.readBlockSuspend(ByteBufferChannel.kt:1713)
at io.ktor.utils.io.ByteBufferChannel.access$readBlockSuspend(ByteBufferChannel.kt:23)
at io.ktor.utils.io.ByteBufferChannel$readBlockSuspend$1.invokeSuspend(ByteBufferChannel.kt)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
I "fixed" it by catching EOFException
and ignoring it, but then I get:
java.io.EOFException: Failed to parse HTTP response: unexpected EOF
at io.ktor.client.engine.cio.UtilsKt$readResponse$2.invokeSuspend(utils.kt:132)
at (Coroutine boundary. ( )
at io.ktor.client.engine.HttpClientEngine$DefaultImpls.executeWithinCallContext(HttpClientEngine.kt:100)
at io.ktor.client.engine.HttpClientEngine$install$1.invokeSuspend(HttpClientEngine.kt:70)
at io.ktor.client.plugins.HttpSend$DefaultSender.execute(HttpSend.kt:138)
at io.ktor.client.plugins.HttpRequestRetry$intercept$1.invokeSuspend(HttpRequestRetry.kt:291)
at io.ktor.client.plugins.HttpRedirect$Plugin$install$1.invokeSuspend(HttpRedirect.kt:61)
at io.ktor.client.plugins.HttpCallValidator$Companion$install$3.invokeSuspend(HttpCallValidator.kt:147)
at io.ktor.client.plugins.HttpSend$Plugin$install$1.invokeSuspend(HttpSend.kt:104)
at io.ktor.client.engine.HttpClientEngine$DefaultImpls.executeWithinCallContext(HttpClientEngine.kt:100)
at io.ktor.client.engine.HttpClientEngine$install$1.invokeSuspend(HttpClientEngine.kt:70)
at io.ktor.client.plugins.HttpSend$DefaultSender.execute(HttpSend.kt:138)
at io.ktor.client.plugins.HttpRequestRetry$intercept$1.invokeSuspend(HttpRequestRetry.kt:291)
at io.ktor.client.plugins.HttpRedirect$Plugin$install$1.invokeSuspend(HttpRedirect.kt:61)
at io.ktor.client.plugins.HttpCallValidator$Companion$install$3.invokeSuspend(HttpCallValidator.kt:147)
at io.ktor.client.plugins.HttpSend$Plugin$install$1.invokeSuspend(HttpSend.kt:104)
at io.ktor.client.plugins.HttpCallValidator$Companion$install$1.invokeSuspend(HttpCallValidator.kt:126)
at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install$1.invokeSuspend(HttpRequestLifecycle.kt:35)
at io.ktor.client.HttpClient.execute$ktor_client_core(HttpClient.kt:191)
at io.ktor.client.statement.HttpStatement.executeUnsafe(HttpStatement.kt:108)
at io.ktor.client.statement.HttpStatement.execute(HttpStatement.kt:47)
at org.jetbrains.intellij.build.KtorKt$downloadFileToCacheLocation$$inlined$useWithScope2$1.invokeSuspend(ktor.kt:114)
So, I guess it is some kind of regression, since code was working previously. Probably the result of https://youtrack.jetbrains.com/issue/KTOR-5252 fix.