Changelog 2.1 version
2.1.3
released 28th October 2022
Client
Cookies: Invalid encoding of cookies' values since 1.4.0
Hello
I have issue on Ktor client 1.4.0 Android (iOS is ok).
I have response with
Content-Type: application/x-plist; charset=utf-8
Set-Cookie: JSESSIONID=jc1wDGgCjR8s72-xdZYYZsLywZdCsiIT86U7X5h7.front10; secure; HttpOnly
But for next the request my Cookie looks like
Cookie: JSESSIONID=jc1wDGgCjR8s72%2DxdZYYZsLywZdCsiIT86U7X5h7%2Efront10;
As I you see dot now encoded
In Ktor 1.3.2 for iOS and Android platforms was ok
Websockets: timeout doesn't cause closing of incoming and outgoing channels
I'm developing network App using Ktor 1.2.5 and Kotlin 1.3.61 in Android 8
When netty websocket server's wifi is turned off by manually (or Ethernet cable unplugged manually), any event or exception is not happened in opposite websocket client even something is sent. Which mean websocket client does not know session is lost.
On the contrary, when websocket client's wifi is turned off, webserver can catch it because closedException: ClosedSendChannelException is occured when something is sent.
Does the any way for websocket client to know "session is lost" when server's network is disconnect?
(When webserver's Ethernet cable is unplugged, websocket client cannot be even closed normally - In other words, final block cannot be called.-
Here is code
-----------------------------------------------------------------
SERVER
-----------------------------------------------------------------
embeddedServer(Netty, port = 8080) {
install(DefaultHeaders)
install(CallLogging)
install(WebSockets) {
pingPeriod = Duration.ofSeconds(10)
timeout = Duration.ofSeconds(5)
}
...
routing {
webSocket("/test") {
try {
incoming.consumeEach { frame ->
if(frame is Frame.Text) {
Log.d("test", frame.readText())
}
}
} finally {
val reason: CloseReason? = closeReason.await()
Log.d("test", reason.toString())
}
}
}
}
private suspend fun sendSomething(data:String) {
try {
clients.send(Frame.Binary(true, data))
}
catch (closedException: ClosedSendChannelException) {
Log.d("test", "Server ClosedSendChannelException is happened")
}
}
-----------------------------------------------------------------
CLIENT
-----------------------------------------------------------------
CoroutineScope(Dispatchers.IO).launch {
try {
client.ws(host = ip, port = 8080, path = "/test") {
pingIntervalMillis = 1000*10
timeoutMillis = 1000*5
...
try {
incoming.consumeEach { frame ->
if(frame is Frame.Text) {
Log.d("test", frame.readText())
}
}
} catch (e: ClosedReceiveChannelException) {
Log.d("test", "ClosedSendChannelException is happened")
} catch (e: Throwable) {
Log.d("test", "ClosedSendChannelException is happened")
} finally {
// This block cannot be called even websocket client run explicitly disconnect function, when webserver's Ethernet cable is unplugged.
val reason: CloseReason? = closeReason.await()
Log.d("test", reason.toString())
}
}
} catch (e: Exception) {
if (e is NoRouteToHostException || e is ConnectException) {
Log.d("test", "Exception")
} else if (e.javaClass.`package`?.name.toString().startsWith("java.net")) {
Log.d("test", "java.net")
} else {
Log.d("test", "unknown")
}
}
}
private suspend fun sendSomething(data:String) {
try {
clients.send(Frame.Binary(true, data))
}
catch (closedException: ClosedSendChannelException) {
Log.d("test", "Client ClosedSendChannelException is happened")
}
}
CIO: A request through a proxy server results in 403 from Cloudflare
To reproduce run the following code:
val client = HttpClient(CIO) {
engine {
proxy = ProxyBuilder.http("http://3.127.33.188:3128") // public proxy server
}
}
val response = client.get("http://eu.kith.com/products.json")
println(response.status)
As a result, the 403 status code is returned from the Cloudflare. The same code with the OkHttp
engine works as expected and a server replies with a 200 status code.
Core
RFC 3986 recommendation for encoding URI is NOT followed
According to https://tools.ietf.org/html/rfc3986#section-2.3
For consistency, percent-encoded octets in the ranges of ALPHA
(%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D), period (%2E),
underscore (%5F), or tilde (%7E) should not be created by URI
producers and, when found in a URI, should be decoded to their
corresponding unreserved characters by URI normalizers.
But currently, Ktor client percent-encodes the hyphen, period, underscore and tilde.
This issue causes other issues like this: https://youtrack.jetbrains.com/issue/KTOR-917
So that the String.encodeURLQueryComponent() and String.encodeURLPath()
should be updated accordinglly. (Perhaps there are other places)
JS: window.location.origin returns null when executed in iframe via srcdoc attribute
Interactive maps in https://github.com/JetBrains/lets-plot after updating ktor
from 1.6.8
to 2.1.1
stopped loading tiles via websocket in https://datalore.jetbrains.com/ in both Chrome and Firefox. Yet tiles work well in kaggle, colab, jupyter, deepnote, nextjournal.
The last line to be executed is:
https://github.com/JetBrains/lets-plot/blob/master/gis/src/commonMain/kotlin/jetbrains/gis/tileprotocol/socket/TileWebSocket.kt#L24
Code inside this receiver never runs in datalore. There are no errors, no logs with engine { this@HttpClient.developmentMode = true }
, even no connection in devTools/Network tab.
Sadly I can't provide any example and you can't test it by yourself.
ByteReadChannel is unable to read files with long lines
We're using ktor and stores session data in redis. To avoid problems with the contents of the session we're base64'ing the data before storing it. This results in a one line file, 16kb big. This data is read back using ByteReadChannel, which apparently can not handle such long lines.
Tested on 1.4.21 and 1.4.32.
Test to trigger the exception:
@Test
fun decodeSessionData() {
val fileContent = ByteBufferChannelTest::class.java.getResource("/16kb-on-one-line.txt").readText()
val channel = ByteReadChannel(fileContent.toByteArray())
// ktor session storage is using channel.readUTF8Line() to read session data
runBlocking {
channel.readUTF8Line()
}
}
Stacktrace on 1.4.21:
io.ktor.utils.io.charsets.TooLongLineException: Line is longer than limit
at io.ktor.utils.io.ByteBufferChannel$readUTF8LineToUtf8Suspend$2.invokeSuspend(ByteBufferChannel.kt:2080)
at io.ktor.utils.io.ByteBufferChannel$readUTF8LineToUtf8Suspend$2.invoke(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteBufferChannel.lookAheadSuspend$suspendImpl(ByteBufferChannel.kt:1825)
at io.ktor.utils.io.ByteBufferChannel.lookAheadSuspend(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteBufferChannel.readUTF8LineToUtf8Suspend(ByteBufferChannel.kt:2064)
at io.ktor.utils.io.ByteBufferChannel.readUTF8LineToAscii(ByteBufferChannel.kt:1999)
at io.ktor.utils.io.ByteBufferChannel.readUTF8LineTo$suspendImpl(ByteBufferChannel.kt:2099)
at io.ktor.utils.io.ByteBufferChannel.readUTF8LineTo(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteBufferChannel.readUTF8Line$suspendImpl(ByteBufferChannel.kt:2103)
at io.ktor.utils.io.ByteBufferChannel.readUTF8Line(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteReadChannelKt.readUTF8Line(ByteReadChannel.kt:223)
Stacktrace on 1.4.32:
io.ktor.utils.io.charsets.TooLongLineException: Line is longer than limit
at io.ktor.utils.io.ByteBufferChannel$readUTF8LineToUtf8Suspend$2.invokeSuspend(ByteBufferChannel.kt:2094)
at io.ktor.utils.io.ByteBufferChannel$readUTF8LineToUtf8Suspend$2.invoke(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteBufferChannel.lookAheadSuspend$suspendImpl(ByteBufferChannel.kt:1827)
at io.ktor.utils.io.ByteBufferChannel.lookAheadSuspend(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteBufferChannel.readUTF8LineToUtf8Suspend(ByteBufferChannel.kt:2076)
at io.ktor.utils.io.ByteBufferChannel.readUTF8LineToAscii(ByteBufferChannel.kt:2011)
at io.ktor.utils.io.ByteBufferChannel.readUTF8LineTo$suspendImpl(ByteBufferChannel.kt:2113)
at io.ktor.utils.io.ByteBufferChannel.readUTF8LineTo(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteBufferChannel.readUTF8Line$suspendImpl(ByteBufferChannel.kt:2117)
at io.ktor.utils.io.ByteBufferChannel.readUTF8Line(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteReadChannelKt.readUTF8Line(ByteReadChannel.kt:223)
Docs
Fix the Kweet sample
On an attempt to run the Kweet sample, the following error occurred:
Exception in thread "main" java.lang.NoSuchMethodError: 'org.h2.engine.SessionInterface org.h2.jdbc.JdbcConnection.getSession()'
Highlight use of the dispose function for Multipart Uploads in ktor docs.
Currently in the ktor docs there is no mention of the dispose function for multi part uploads, but this function could handle the deletion of temporary files created when large bodies are uploaded as seen here.
From my perspective it would be useful to include that information in the docs as it currently could be easily overlooked. Even the github code sample doesn't include this.
Please let me know if I'm missing something obvious or it is already included somewhere else.
Server
Websockets timeout doesn't cause a close of a connection
When setting a timeout on a ktor server websocket, the connection does not get closed once the timeout happens, if the client does not respond. It works as intended, if the timout happens, because the client is too slow to repond to a ping frame. It however does not work as intended, if the timeeout happens because the client lost their internet connection (in my case simulated by unplugging the LAN cable on the client machine).
What I expect to happen: When I set a ping and timeout on a websocket connection and the client loses the connection to the server, the timeout occures and I can no longer send Frames on the outgoing channel and any receive on the incoming channel results in a ClosedReceiveChannelException
What is actually happening: Once the client loses a connection, the timeout occurs, but a receive call is blocking as if the connection was still open. Sending frames on the outgoing channel does not result in an exception either.
What I think is the reason for this behaviour: Once the timeout occurs, the server sends a Close frame to the client and waits for the clients close acknowledgement. Since the client lost its connection, the close is not acknowledged and the server keeps the connection open.
This issue can be reproduced with the following code (connect to the websocket and remove the network cable between the server and the client):
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(1)
timeout = Duration.ofSeconds(1)
}
routing {
webSocket("/socket") {
print("Connection")
while(true) {
withTimeoutOrNull(1000) {
println(incoming.receive())
}
outgoing.send(Frame.Text("test"))
}
}
}
}.start(wait = true)
}
I would expect that after the client lost the connection, either the incoming.receive()
or the outgoing.send()
should throw an exception, but neither happens.
HOCON: CLI parameters don't override custom properties since 2.1.0
Version 2.0.3 is OK.
I've tested it with HOCON application.conf
file.
application.conf
example:
ktor {
deployment {
port = 8085
}
}
some {
custom {
prop = ololo
}
}
Command example:
java -jar build/libs/app.jar -P:some.custom.prop=azaza
Expected bevaviour:
val n = config.config("some.custom").property("prop")
// n is "azaza" (from command line)
Actual behaviour:
val n = config.config("some.custom").property("prop")
// n is "ololo" (default value from file)
It wouldn't be so bad if there was a better way to pass parameters. But the alternative is either the whole file or the use of environment variables, both of which are much, much less convenient.
Autoreloading: "Flow invariant is violated" error since Ktor 2.0.3
When trying to upgrade from Ktor 2.0.2 to 2.0.3 (and all the way to 2.1.2), we started encountering strange Flow invariant violations. The stacktrace looks something like this:
java.lang.IllegalStateException: Flow invariant is violated:
Flow was collected in [io.sentry.kotlin.SentryContext@51f480a5, com.corp.app.ApplicationKt$module$2$1$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@4652be27, CoroutineId(6), \"coroutine#6\":ScopeCoroutine{Active}@4f97a3bd, io.ktor.server.engine.ClassLoaderAwareContinuationInterceptor@3b8c8a07],
but emission happened in [io.sentry.kotlin.SentryContext@51f480a5, com.corp.app.ApplicationKt$module$2$1$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@4652be27, CoroutineId(6), \"coroutine#6\":ScopeCoroutine{Active}@4f97a3bd, io.ktor.server.engine.ClassLoaderAwareContinuationInterceptor@3b8c8a07].
Please refer to 'flow' documentation or use 'flowOn' instead
at kotlinx.coroutines.flow.internal.SafeCollector_commonKt.checkContext(SafeCollector.common.kt:85)
at kotlinx.coroutines.flow.internal.SafeCollector.checkContext(SafeCollector.kt:106)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:83)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:66)
at com.corp.app.extensions.aws.SqsKt$getFlowFor$1.invokeSuspend(Sqs.kt:33)
at \u0008\u0008\u0008(Coroutine boundary.\u0008(\u0008)
at com.corp.app.ApplicationKt$module$2$1$1$1.invokeSuspend(Application.kt:172)
at \u0008\u0008\u0008(Coroutine creation stacktrace.\u0008(\u0008)
at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:122)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47)
at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source)
at com.corp.app.ApplicationKt$module$2$1$1.invokeSuspend(Application.kt:171)
at com.corp.app.ApplicationKt$module$2$1$1.invoke(Application.kt)
at com.corp.app.ApplicationKt$module$2$1$1.invoke(Application.kt)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
at kotlinx.coroutines.SupervisorKt.supervisorScope(Supervisor.kt:61)
at com.corp.app.ApplicationKt$module$2.invokeSuspend(Application.kt:166)
[...]
After trying all kinda ways trying to fix coroutine contexts, I found KTOR-4164 in the changelog and tried setting ktor.development = false
in my config which significantly changes behavior on this (makes it much less reproducible on my side). I think this may be an unintended regression of the "fix" for the other issue?
Just in case it's an issue with my code, the method in question looks like this (uses the AWS Java SDK):
fun <T> SqsAsyncClient.getFlowFor(queueUrl: String, klass: Class<T>) = flow {
val queue = this@getFlowFor
val flow = this
while (currentCoroutineContext().isActive) {
val response = queue.receiveMessage(
ReceiveMessageRequest.builder()
.queueUrl(queueUrl)
.maxNumberOfMessages(10)
.waitTimeSeconds(20)
.build()
).await()
if (!response.hasMessages()) continue
val messages = response.messages()
messages
.map { json.readValue(it.body(), klass) }
.forEach { flow.emit(it) }
}
}
SensitivityWatchEventModifier - Move the reflection call of this modifier out from the Ktor Core
Due to the certain restrictions in Android, this class (or even a reference) is on Google's blacklist. Is it possible to consider moving it out from the framework code? Or maybe provide some configurable mechanism to avoid the explicitly mentioned class in a code?
Issue details:
Android Documentation: https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces
Source code: https://github.com/ktorio/ktor/blob/ef4e65cd48ca370528390d1a5a35278ba42b3bf3/ktor-server/ktor-server-host-common/jvm/src/io/ktor/server/engine/internal/AutoReloadUtils.kt#L68
The veridex test mentioned in the documentation doesn't pass successfully with the following message:
Reflection blacklist Lcom/sun/nio/file/SensitivityWatchEventModifier;->HIGH use(s):
Lio/ktor/server/engine/ApplicationEngineEnvironmentReloading;->get_com_sun_nio_file_SensitivityWatchEventModifier_HIGH()Ljava/nio/file/WatchEvent$Modifier;
This issue prevents Ktor Core to be fully compatible with Android and also breaks Google's requirements for building a custom Android-based device.
Thanks in advance!
DefaultHeaders: a header is duplicated in a StatusPages's handler
The HTTP headers configured via the DefaultHeaders plugin will be duplicated in the response if handled by a StatusPages Plugin status handler.
Example application:
package com.example
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.plugins.defaultheaders.DefaultHeaders
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
fun main(args: Array<String>): Unit =
io.ktor.server.cio.EngineMain.main(args)
fun Application.module() {
install(DefaultHeaders) {
header("Foo", "Bar")
}
install(StatusPages) {
status(HttpStatusCode.NotFound) { call, httpStatus ->
call.respond(httpStatus, "error")
}
}
routing {
get("/") {
call.respondText("Hello World!")
}
}
}
With a request to an invalid URL, like GET http://localhost:8080/invalid-url
you will receive the following response:
HTTP/1.1 404 Not Found
Foo: Bar
Date: Fri, 14 Oct 2022 09:03:55 GMT
Server: Ktor/2.1.2
Foo: Bar
Content-Length: 5
Content-Type: text/plain; charset=UTF-8
Connection: keep-alive
error
As you can see, the header Foo: Bar
is duplicated.
A request to the valid URL GET http://localhost:8080/
does not lead to the duplicated header:
HTTP/1.1 200 OK
Foo: Bar
Date: Fri, 14 Oct 2022 09:23:22 GMT
Server: Ktor/2.1.2
Content-Length: 12
Content-Type: text/plain; charset=UTF-8
Connection: keep-alive
Hello World!
Netty HTTP/2: response headers contain ":status" header and that leads to IllegalHeaderNameException in the ConditionalHeaders plugin
This causes an exception io.ktor.http.IllegalHeaderNameException: Header name ':status' contains illegal character ':' (code 58)
when processing ConditionalHeaders
.
Non-SSL connector is not affected.
Version 2.0.3 is not affected, 2.1.1 and 2.1.2 are.
I'm using the Netty engine.
Maven: ktor-server-test-host-jvm causes dependency error starting from Ktor 2.0.3
When trying to test my ktor application (using v2.1.1), maven produces an error:
Could not find artifact org.jetbrains.kotlinx:kotlinx-coroutines-bom:jar:1.6.3
Using that dependency directly, without using "ktor-server-test-host-jvm" works. But as soon as I depend on ktor-server-test-host-jvm:2.1.1 I get this error.
The same issue doesn't happen with version 2.0.0.
Autoreloading: ClassCastException when retrieving plugins in testApplication
Hello, while working on creating a Ktor plugin and setting up tests for it, I've encountered the following bug:
When creating a test that directly retrieves a plugin (either via the Application.plugin() function or when using custom extension functions over Application), the code crashes with a ClassCastException. While the class is the exact same, the instance of the class returned by the test Ktor application comes from another classloader, leading to the exceptions you can see below. It seems to be related to the development mode's class reloading feature interfering somewhere along the line.
Repro
- Here's a GitHub repository to repro this issue: https://github.com/utybo/cce_ktor_test
- Here's a GitHub Actions run with the failing tests: https://github.com/utybo/cce_ktor_test/runs/7724799083?check_suite_focus=true#step:5:34
Here's an example, let's take this plugin:
class MyCalculatorPlugin {
class Configuration
companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, MyCalculatorPlugin> {
override val key = AttributeKey<MyCalculatorPlugin>("MyCalculatorPlugin")
override fun install(
pipeline: ApplicationCallPipeline,
configure: Configuration.() -> Unit
): MyCalculatorPlugin {
return MyCalculatorPlugin()
}
}
fun add(x: Int, y: Int): Int {
return x + y
}
}
val Application.myCalculator get() = plugin(MyCalculatorPlugin)
The following tests will fail:
@Test
fun `Test simple sum via 'myCalculator' utility property`() = testApplication {
install(MyCalculatorPlugin)
application {
val result = myCalculator.add(1, 2) // <-- ClassCastException
assertEquals(3, result)
}
}
@Test
fun `Test simple sum via 'plugin' function`() = testApplication {
install(MyCalculatorPlugin)
application {
val result = plugin(MyCalculatorPlugin).add(1, 2) // <-- ClassCastException
assertEquals(3, result)
}
}
Stack trace for the first test:
class org.example.ccektortest.MyCalculatorPlugin cannot be cast to class org.example.ccektortest.MyCalculatorPlugin (org.example.ccektortest.MyCalculatorPlugin is in unnamed module of loader 'app'; org.example.ccektortest.MyCalculatorPlugin is in unnamed module of loader io.ktor.server.engine.OverridingClassLoader$ChildURLClassLoader @68ad99fe)
java.lang.ClassCastException: class org.example.ccektortest.MyCalculatorPlugin cannot be cast to class org.example.ccektortest.MyCalculatorPlugin (org.example.ccektortest.MyCalculatorPlugin is in unnamed module of loader 'app'; org.example.ccektortest.MyCalculatorPlugin is in unnamed module of loader io.ktor.server.engine.OverridingClassLoader$ChildURLClassLoader @68ad99fe)
at org.example.ccektortest.LibraryKt.getMyCalculator(Library.kt:28)
at org.example.ccektortest.LibraryTest$Test simple sum via 'myCalculator' utility property$1$1.invoke(LibraryTest.kt:17)
at org.example.ccektortest.LibraryTest$Test simple sum via 'myCalculator' utility property$1$1.invoke(LibraryTest.kt:16)
at io.ktor.server.engine.internal.CallableUtilsKt.executeModuleFunction(CallableUtils.kt:51)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$launchModuleByName$1.invoke(ApplicationEngineEnvironmentReloading.kt:334)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$launchModuleByName$1.invoke(ApplicationEngineEnvironmentReloading.kt:333)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.avoidingDoubleStartupFor(ApplicationEngineEnvironmentReloading.kt:358)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.launchModuleByName(ApplicationEngineEnvironmentReloading.kt:333)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.access$launchModuleByName(ApplicationEngineEnvironmentReloading.kt:32)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$instantiateAndConfigureApplication$1.invoke(ApplicationEngineEnvironmentReloading.kt:321)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$instantiateAndConfigureApplication$1.invoke(ApplicationEngineEnvironmentReloading.kt:312)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.avoidingDoubleStartup(ApplicationEngineEnvironmentReloading.kt:340)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.instantiateAndConfigureApplication(ApplicationEngineEnvironmentReloading.kt:312)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.createApplication(ApplicationEngineEnvironmentReloading.kt:149)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.start(ApplicationEngineEnvironmentReloading.kt:279)
at io.ktor.server.testing.TestApplicationEngine.start(TestApplicationEngine.kt:125)
at io.ktor.server.engine.ApplicationEngine$DefaultImpls.start$default(ApplicationEngine.kt:68)
at io.ktor.server.testing.TestApplicationKt.testApplication(TestApplication.kt:267)
at org.example.ccektortest.LibraryTest.Test simple sum via 'myCalculator' utility property(LibraryTest.kt:14)
[snip]
Stack trace for the second test:
class org.example.ccektortest.MyCalculatorPlugin cannot be cast to class org.example.ccektortest.MyCalculatorPlugin (org.example.ccektortest.MyCalculatorPlugin is in unnamed module of loader 'app'; org.example.ccektortest.MyCalculatorPlugin is in unnamed module of loader io.ktor.server.engine.OverridingClassLoader$ChildURLClassLoader @5b057c8c)
java.lang.ClassCastException: class org.example.ccektortest.MyCalculatorPlugin cannot be cast to class org.example.ccektortest.MyCalculatorPlugin (org.example.ccektortest.MyCalculatorPlugin is in unnamed module of loader 'app'; org.example.ccektortest.MyCalculatorPlugin is in unnamed module of loader io.ktor.server.engine.OverridingClassLoader$ChildURLClassLoader @5b057c8c)
at org.example.ccektortest.LibraryTest$Test simple sum via 'plugin' function$1$1.invoke(LibraryTest.kt:26)
at org.example.ccektortest.LibraryTest$Test simple sum via 'plugin' function$1$1.invoke(LibraryTest.kt:25)
at io.ktor.server.engine.internal.CallableUtilsKt.executeModuleFunction(CallableUtils.kt:51)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$launchModuleByName$1.invoke(ApplicationEngineEnvironmentReloading.kt:334)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$launchModuleByName$1.invoke(ApplicationEngineEnvironmentReloading.kt:333)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.avoidingDoubleStartupFor(ApplicationEngineEnvironmentReloading.kt:358)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.launchModuleByName(ApplicationEngineEnvironmentReloading.kt:333)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.access$launchModuleByName(ApplicationEngineEnvironmentReloading.kt:32)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$instantiateAndConfigureApplication$1.invoke(ApplicationEngineEnvironmentReloading.kt:321)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$instantiateAndConfigureApplication$1.invoke(ApplicationEngineEnvironmentReloading.kt:312)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.avoidingDoubleStartup(ApplicationEngineEnvironmentReloading.kt:340)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.instantiateAndConfigureApplication(ApplicationEngineEnvironmentReloading.kt:312)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.createApplication(ApplicationEngineEnvironmentReloading.kt:149)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.start(ApplicationEngineEnvironmentReloading.kt:279)
at io.ktor.server.testing.TestApplicationEngine.start(TestApplicationEngine.kt:125)
at io.ktor.server.engine.ApplicationEngine$DefaultImpls.start$default(ApplicationEngine.kt:68)
at io.ktor.server.testing.TestApplicationKt.testApplication(TestApplication.kt:267)
at org.example.ccektortest.LibraryTest.Test simple sum via 'plugin' function(LibraryTest.kt:23)
[snip]
Workaround
Disable development mode in tests by adding the following line inside the testApplication
block:
environment { developmentMode = false }
Versions
- OpenJDK 17 (also repro'd with Temurin JDK 17)
- Ktor 2.0.3
- Kotlin 1.7.10
- Gradle 7.5
Shared
WebSocketDeflateExtension configureProtocols always failed with stackOverflow
In this extension code looks weird. I guess, this was developed to save a previous manualConfiguration, but now it calls themselves infinitely.
internal var manualConfig: (MutableList<WebSocketExtensionHeader>) -> Unit = {}
/**
* Configure which protocols should send the client.
*/
public fun configureProtocols(block: (protocols: MutableList<WebSocketExtensionHeader>) -> Unit) {
manualConfig = {
manualConfig(it)
block(it)
}
}
There should be a list of manualConf
to do so.
internal val manualConfig: List<(MutableList<WebSocketExtensionHeader>) -> Unit> = mutableList()
/**
* Configure which protocols should send the client.
*/
public fun configureProtocols(block: (protocols: MutableList<WebSocketExtensionHeader>) -> Unit) {
manualConfig.add(block)
}
// Usage
internal fun build(): List<WebSocketExtensionHeader> {
val result = mutableListOf<WebSocketExtensionHeader>()
val parameters = mutableListOf<String>()
if (clientNoContextTakeOver) {
parameters += CLIENT_NO_CONTEXT_TAKEOVER
}
if (serverNoContextTakeOver) {
parameters += SERVER_NO_CONTEXT_TAKEOVER
}
result += WebSocketExtensionHeader(PERMESSAGE_DEFLATE, parameters)
manualConfig.forEach { configure ->
configure(result)
}
return result
}
Other
Update Kotlin to 1.7.20
CIO engine has wrong doc for request timeout
Request timeout handles the whole request time, not until body starts to download