Changelog 3.4 version
3.4.1
released 4th March 2026
Client
Flow invariant error happens after update to Ktor 3.4.0
See thread:
https://jetbrains.slack.com/archives/C07U498LLUR/p1771261810475639
It seems that flows fetched from the Grazie AI platform client are failing upon collection due to a flow invariance exception.
This exception is happening because we are supplying a dispatcher for running the execute {} block and the grazie client is emitting inside this. We'll either need to revert to using the caller's context or fix the approach in the grazie library.
Curl: Undefined symbol errors when linking on Linux since 3.4.0
Originating job: https://github.com/LagrangeDev/acidify/actions/runs/21320047010/job/61368571336
It seems that the problem comes from the Curl engine. The same codebase compiles for Ktor 3.3.3.
e: /home/runner/.konan/dependencies/llvm-19-x86_64-linux-essentials-103/bin/ld.lld invocation reported errors
The /home/runner/.konan/dependencies/llvm-19-x86_64-linux-essentials-103/bin/ld.lld command returned non-zero exit code: 1.
output:
ld.lld: error: undefined symbol: __aarch64_swp4_acq
>>> referenced by easy.c
>>> easy.c.o:(curl_global_init) in archive /tmp/included11785227692859483917/libcurl.a
>>> referenced by easy.c
>>> easy.c.o:(curl_global_init_mem) in archive /tmp/included11785227692859483917/libcurl.a
>>> referenced by easy.c
>>> easy.c.o:(curl_global_cleanup) in archive /tmp/included11785227692859483917/libcurl.a
>>> referenced 3 more times
ld.lld: error: undefined symbol: __aarch64_ldadd4_rel
>>> referenced by kdf_legacy_kmgmt.c
>>> libdefault-lib-kdf_legacy_kmgmt.o:(ossl_kdf_data_free) in archive /tmp/included11785227692859483917/libcrypto.a
>>> referenced by mac_legacy_kmgmt.c
>>> libdefault-lib-mac_legacy_kmgmt.o:(ossl_mac_key_free) in archive /tmp/included11785227692859483917/libcrypto.a
>>> referenced by x509_lu.c
>>> libcrypto-lib-x509_lu.o:(X509_STORE_free) in archive /tmp/included11785227692859483917/libcrypto.a
>>> referenced 56 more times
ld.lld: error: undefined symbol: __aarch64_ldadd4_relax
>>> referenced by kdf_legacy_kmgmt.c
>>> libdefault-lib-kdf_legacy_kmgmt.o:(ossl_kdf_data_up_ref) in archive /tmp/included11785227692859483917/libcrypto.a
>>> referenced by mac_legacy_kmgmt.c
>>> libdefault-lib-mac_legacy_kmgmt.o:(ossl_mac_key_up_ref) in archive /tmp/included11785227692859483917/libcrypto.a
>>> referenced by x509cset.c
>>> libcrypto-lib-x509cset.o:(X509_CRL_up_ref) in archive /tmp/included11785227692859483917/libcrypto.a
>>> referenced 72 more times
ld.lld: error: undefined symbol: __aarch64_ldadd8_relax
>>> referenced by threads_pthread.c
>>> libcrypto-lib-threads_pthread.o:(ossl_rcu_read_lock) in archive /tmp/included11785227692859483917/libcrypto.a
ld.lld: error: undefined symbol: __aarch64_ldadd8_acq
>>> referenced by threads_pthread.c
>>> libcrypto-lib-threads_pthread.o:(ossl_rcu_read_lock) in archive /tmp/included11785227692859483917/libcrypto.a
ld.lld: error: undefined symbol: __aarch64_ldadd8_rel
>>> referenced by threads_pthread.c
>>> libcrypto-lib-threads_pthread.o:(ossl_rcu_read_unlock) in archive /tmp/included11785227692859483917/libcrypto.a
>>> referenced by threads_pthread.c
>>> libcrypto-lib-threads_pthread.o:(ossl_synchronize_rcu) in archive /tmp/included11785227692859483917/libcrypto.a
ld.lld: error: undefined symbol: __aarch64_ldadd4_acq_rel
>>> referenced by threads_pthread.c
>>> libcrypto-lib-threads_pthread.o:(CRYPTO_atomic_add) in archive /tmp/included11785227692859483917/libcrypto.a
ld.lld: error: undefined symbol: __aarch64_ldadd8_acq_rel
>>> referenced by threads_pthread.c
>>> libcrypto-lib-threads_pthread.o:(CRYPTO_atomic_add64) in archive /tmp/included11785227692859483917/libcrypto.a
ld.lld: error: undefined symbol: __aarch64_ldclr8_acq_rel
>>> referenced by threads_pthread.c
>>> libcrypto-lib-threads_pthread.o:(CRYPTO_atomic_and) in archive /tmp/included11785227692859483917/libcrypto.a
ld.lld: error: undefined symbol: __aarch64_ldset8_acq_rel
>>> referenced by threads_pthread.c
>>> libcrypto-lib-threads_pthread.o:(CRYPTO_atomic_or) in archive /tmp/included11785227692859483917/libcrypto.a
NodeJS CIO: "Module 'os' could not be imported" error on resolving WORKING_DIRECTORY_PATH with es2015 target
Steps to reproduce
See the attached example which
- Create a Ktor server with a JS target and the CIO engine
- Configures the JS target
target.set("es2015") - Run the tests
Results
UnsupportedOperationException: Module 'os' could not be imported
at <global>.captureStack(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/js/runtime/coreRuntime.kt:146)
at <global>.init_kotlin_UnsupportedOperationException(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/kotlin-kotlin-stdlib.mjs:18594)
at Function.new_kotlin_UnsupportedOperationException_iaim4v(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/kotlin-kotlin-stdlib.mjs:3932)
at <global>.os$delegate$lambda(file:///opt/buildAgent/work/22e3a7db6b861ba7/core/js/src/node/nodeModulesJs.kt:28)
at UnsafeLazyImpl.get_value_j01efc(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/src/kotlin/util/Lazy.kt:100)
at <global>.get_os(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/src/kotlin/util/Lazy.kt:52)
at <global>._init_properties_FileSystemNodeJs_kt__m4c3u(file:///opt/buildAgent/work/22e3a7db6b861ba7/core/nodeFilesystemShared/src/files/FileSystemNodeJs.kt:131)
at <global>.get_SystemFileSystem(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/kotlinx-io-kotlinx-io-core.mjs:3879)
at <global>._init_properties_ServerEngineUtils_kt__jiy5i2(file:///mnt/agent/work/8d547b974a7be21f/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/ServerEngineUtils.kt:9)
at <global>.get_WORKING_DIRECTORY_PATH(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/ktor-ktor-server-core.mjs:10464)
at Function.new_io_ktor_server_application_ServerConfigBuilder_dbani6(file:///mnt/agent/work/8d547b974a7be21f/ktor-server/ktor-server-core/common/src/io/ktor/server/application/Application.kt:35)
at <global>.serverConfig(file:///mnt/agent/work/8d547b974a7be21f/ktor-server/ktor-server-core/common/src/io/ktor/server/application/Application.kt:108)
at <global>.<unknown>(file:///mnt/agent/work/8d547b974a7be21f/ktor-server/ktor-server-test-host/common/src/io/ktor/server/testing/TestApplication.kt:205)
at UnsafeLazyImpl.get_value_j01efc(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/src/kotlin/util/Lazy.kt:100)
at ApplicationTestBuilder.get_properties_7bgu43(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/src/kotlin/util/Lazy.kt:52)
at <global>.<unknown>(file:///mnt/agent/work/8d547b974a7be21f/ktor-server/ktor-server-test-host/common/src/io/ktor/server/testing/TestApplication.kt:215)
at UnsafeLazyImpl.get_value_j01efc(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/src/kotlin/util/Lazy.kt:100)
at ApplicationTestBuilder.get_embeddedServer_t1rk3r(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/src/kotlin/util/Lazy.kt:52)
at <global>.<unknown>(file:///mnt/agent/work/8d547b974a7be21f/ktor-server/ktor-server-test-host/common/src/io/ktor/server/testing/TestApplication.kt:399)
at <global>.<unknown>(file:///mnt/agent/work/8d547b974a7be21f/ktor-server/ktor-server-test-host/common/src/io/ktor/server/testing/TestApplication.kt:85)
at UnsafeLazyImpl.get_value_j01efc(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/src/kotlin/util/Lazy.kt:100)
at TestApplication.get_server_izkd7n(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/src/kotlin/util/Lazy.kt:52)
at .doResume_5yljmg(file:///mnt/agent/work/8d547b974a7be21f/ktor-server/ktor-server-test-host/common/src/io/ktor/server/testing/TestApplication.kt:103)
at TestApplication.start_o8toy3(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/ktor-ktor-server-test-host.mjs:688)
at .doResume_5yljmg(file:///mnt/agent/work/8d547b974a7be21f/ktor-server/ktor-server-test-host/common/src/io/ktor/server/testing/client/DelegatingTestClientEngine.kt:48)
at DelegatingTestClientEngine.execute_bvjlbk(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/ktor-ktor-server-test-host.mjs:2987)
at slambda.doResume_5yljmg(file:///mnt/agent/work/8d547b974a7be21f/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngine.kt:183)
at slambda.invoke_hvk5sg(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/ktor-ktor-client-core.mjs:1429)
at <global>.l(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/ktor-ktor-client-core.mjs:10297)
at 1.doResume_5yljmg(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/js/src/kotlin/coroutines/intrinsics/IntrinsicsJs.kt:194)
at 1.resumeWith_b9cu3x(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/js/src/kotlin/coroutines/CoroutineImpl.kt:42)
at 1.resumeWith_dtxwbr(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/kotlin-kotlin-stdlib.mjs:3555)
at DispatchedContinuation.run_mvkpxh(file:///Users/me/Development/ktor-cio-js-server/build/js/packages/cio-js-server-test/kotlin/src/kotlin/coroutines/Continuation.kt:45)
at ScheduledMessageQueue.process_myqcf5(file:///mnt/agent/work/44ec6e850d5c63f0/kotlinx-coroutines-core/jsAndWasmJsShared/src/internal/JSDispatcher.kt:127)
at <global>.<unknown>(file:///mnt/agent/work/44ec6e850d5c63f0/kotlinx-coroutines-core/js/src/JSDispatcher.kt:21)
at <global>.processTicksAndRejections(node:internal/process/task_queues:77)
CIO engine rejects valid certificates with unsupported signature algorithms
When using the CIO engine, HTTPS/TLS connections can fail during the handshake with:
TLSException: No suitable server certificate received
even though the server presents a valid X.509 chain that the JVM TrustManager would accept.
Root cause is an extra post-validation filter in CIO TLS handshake code: after the TrustManager verifies the chain, CIO tries to pick a “suitable” leaf certificate by comparing the certificate’s signature algorithm OID (X509Certificate.sigAlgOID, i.e., how the CA signed the certificate) against a hardcoded SupportedSignatureAlgorithms list meant for TLS handshake signature negotiation. This conflates two different concepts and incorrectly rejects otherwise-valid certificates whose sigAlgOID is not in that list.
This is observed with certificates signed using algorithms not covered by the allowlist (notably RSA-PSS; also reproducible with SHA224withRSA), leading to connection failures to compliant servers.
Expected behavior
If the certificate chain is accepted by the TrustManager, CIO should accept the leaf certificate regardless of its certificate signature algorithm OID (handshake signature algorithms are negotiated separately).
Actual behavior
CIO rejects the certificate and aborts the handshake with “No suitable server certificate received”.
Impact
Breaks connections to servers using modern/less-common certificate signature algorithms, despite the certificates being valid and trusted by the platform/JVM.
Reproduction
- Configure a TLS server with a certificate whose
sigAlgOIDis not in CIO’s hardcoded supported list (e.g., RSA-PSS; alternatively SHA224withRSA). - Connect to it using a Ktor client with CIO + HTTPS.
- Handshake fails with TLSException: No suitable server certificate received even though standard clients succeed and the JVM would trust the chain.
Created in place of KTOR-8587
Ktor always adds by default an Accept-Charset header
Initially mentioned issue in KTOR-3799:
A similar issue has been going on since 1.6.7 with Ktor (tested on 1.6.7 and 2.2.3).
Specifically, for a HttpClient (CIO engine, in my scenario, as I am working on Android), the
Accept-Charset: UTF-8is forcefully added on any request, if I do not add anyAccept-Charsetheader.The information logged by the client is the following:
METHOD: HttpMethod(value=POST)
COMMON HEADERS
-> Accept: application/json
-> Accept-Charset: UTF-8
-> Accept-Language: en-us
-> Authorization: <...>
-> User-Agent: <...>
CONTENT HEADERS
-> Content-Length: 68
-> Content-Type: application/json
BODY Content-Type: application/json
BODY START
{
"field1": "value1",
"field2": "value2"
}
BODY ENDThe issue comes with newer standards (https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name), where WAFs (Web Application Firewalls) refuse requests that contain the
Accept-Charsetheader.
Curl: Segfaults when working with WebSockets
We have silent test failures in CurlWebSocketTests. A brief investigation has shown that these failures are caused by segfaults.
Further investigation is needed.
StreamResetException is not propagated to the caller of StreamRequestBody.writeTo since 3.4.0
In v3.3.3, within the StreamRequestBody.writeTo() function, a StreamResetException is thrown that is propagated to the caller of writeTo.
However, in v3.4.0 a ClosedByteChannelException is being thrown within the CoroutineScope of the callerContext which uses the SilentSuperVisor and as far as I see this correctly, this implicates that even if the coroutine job is joined (in case of duplex=false), the exception is not propagated to the caller of writeTo.
As a consequence, the execution of that request already throws an exception at this point in OkHttpEngine.excecuteHttpRequest, so it seems that the exception propagation is too early because it does not originate from the OkHttpCallback. The OkHttpCallback is later called with just java.io.IOException: Canceled but where continuation.isCancelled is already true.
This is different compared to v3.3.3, where in this case the OkHttpCallback.onResponse callback is called, the OkHttpEngine.excecuteHttpRequest is resumed and a proper HttpResponseData with the http 401 is returned.
The following example test demonstrates that the error is propagated to the one who owns the callContext, but not to the one who calls the writeTo function, and this is exactly what currently happens in v3.4.0's StreamRequestBody.writeTo (the test succeeds):
@Test
fun writeTo() = runBlocking {
val callContext = Job()
val job = CoroutineScope(callContext).launch(Dispatchers.IO) {
// IO stuff throws error
throw Exception("test error")
}
job.join()
}
WasmJS bad get and set implementations for Uint8Array and ArrayLike
With the current implementation, when they are generated into a JS file, the resulting code is syntactically incorrect, so webpack and JS VMs reject such code even if it's unreachable.
It is mostly unobservable since the declarations are not used inside ktor, and the kotlin toolchain doesn't generate helper functions for them by default. But it breaks projects using ktor when we try to build them with wip per-module compilation.
SendCountExceedException when request is sent twice with maxRetries = 0 since 3.3.2
Stacktrace:
io.ktor.client.plugins.SendCountExceedException: Max send count 1 exceeded. Consider increasing the property maxSendCount if more is required.
at app.impl.network.api.NetworkChatDataSource$sendMessage$1.invokeSuspend(Unknown Source:15)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
at kotlinx.coroutines.UndispatchedCoroutine.afterResume(CoroutineContext.kt:266)
at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:101)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:47)
at kotlinx.coroutines.UndispatchedCoroutine.afterResume(CoroutineContext.kt:266)
at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:101)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:47)
at kotlinx.coroutines.UndispatchedCoroutine.afterResume(CoroutineContext.kt:266)
at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:101)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:47)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
at android.os.Handler.handleCallback(Handler.java:995)
at android.os.Handler.dispatchMessage(Handler.java:103)
at android.os.Looper.loopOnce(Looper.java:273)
at android.os.Looper.loop(Looper.java:363)
at android.app.ActivityThread.main(ActivityThread.java:10060)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:632)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:975)
Caused by: io.ktor.client.plugins.SendCountExceedException: Max send count 1 exceeded. Consider increasing the property maxSendCount if more is required.
at io.ktor.client.plugins.HttpSend$DefaultSender.execute(HttpSend.kt:130)
at io.ktor.client.plugins.api.Send$Sender.proceed(CommonHooks.kt:47)
at io.ktor.client.plugins.HttpTimeoutKt$HttpTimeout$3$1.invokeSuspend(HttpTimeout.kt:168)
at io.ktor.client.plugins.HttpTimeoutKt$HttpTimeout$3$1.invoke(Unknown Source:17)
at io.ktor.client.plugins.HttpTimeoutKt$HttpTimeout$3$1.invoke(Unknown Source:6)
at io.ktor.client.plugins.api.Send$install$1.invokeSuspend(CommonHooks.kt:52)
at io.ktor.client.plugins.api.Send$install$1.invoke(Unknown Source:15)
at io.ktor.client.plugins.api.Send$install$1.invoke(Unknown Source:6)
at io.ktor.client.plugins.HttpSend$InterceptedSender.execute(HttpSend.kt:115)
at io.ktor.client.plugins.api.Send$Sender.proceed(CommonHooks.kt:47)
at io.ktor.client.plugins.HttpRequestRetryKt$HttpRequestRetry$2$2.invokeSuspend(HttpRequestRetry.kt:357)
at io.ktor.client.plugins.HttpRequestRetryKt$HttpRequestRetry$2$2.invoke(Unknown Source:26)
at io.ktor.client.plugins.HttpRequestRetryKt$HttpRequestRetry$2$2.invoke(Unknown Source:6)
at io.ktor.client.plugins.api.Send$install$1.invokeSuspend(CommonHooks.kt:52)
at io.ktor.client.plugins.api.Send$install$1.invoke(Unknown Source:15)
at io.ktor.client.plugins.api.Send$install$1.invoke(Unknown Source:6)
at io.ktor.client.plugins.HttpSend$InterceptedSender.execute(HttpSend.kt:115)
at io.ktor.client.plugins.api.Send$Sender.proceed(CommonHooks.kt:47)
at io.ktor.client.plugins.auth.AuthKt.Auth$lambda$0$executeWithNewToken(Auth.kt:154)
at io.ktor.client.plugins.auth.AuthKt.access$Auth$lambda$0$executeWithNewToken(Auth.kt:1)
at io.ktor.client.plugins.auth.AuthKt$Auth$2$2.invokeSuspend(Auth.kt:189)
The request in question:
client.post<ChatMessage, SendMessageRequest>( // this is just an overload of post<> and then .body<T>()
url = "$Route/$conversationId/messages",
body = SendMessageRequest(
content = text,
id = desiredId,
timezone = TimeZone.currentSystemDefault().id,
)
) {
timeout { noTimeout() }
retry { noRetry() }
}.mapError<ApiError, _> { it.parseOrNull<ChatError>() ?: it }
internal fun HttpTimeoutConfig.noTimeout() = apply {
requestTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS
socketTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS
}
Client setup:
install(Logging) {
// Note: do NOT use logging level ALL because of another bug in ktor that fucks up file uploads
level = LogLevel.INFO
logger = KermitLogger
sanitizeHeader { it == HttpHeaders.Authorization }
}
// MUST be before ContentNegotiation: https://youtrack.jetbrains.com/issue/KTOR-7631/
install(SSE) {
reconnectionTime = 5.seconds
maxReconnectionAttempts = 5
}
install(ContentNegotiation) { json(json) }
install(Auth) {
bearer {
sendWithoutRequest { true }
}
}
install(HttpRequestRetry) {
retryOnServerErrors(1)
constantDelay(500L)
}
install(HttpTimeout) {
requestTimeoutMillis = NetworkDefaults.RequestTimeout
connectTimeoutMillis = NetworkDefaults.ConnectTimeout
socketTimeoutMillis = NetworkDefaults.ConnectTimeout
}
// managed by the engine, otherwise Ktor will crash
// install(HttpCache) {
// isShared = true
// publicStorage(FileStorage(context.cacheDir))
// }
install(DataConversion)
install(ContentEncoding) {
mode = ContentEncodingConfig.Mode.All // compress requests
deflate(1f)
gzip(0.8f)
identity(0.5f)
}
addDefaultResponseValidation()
useDefaultTransformers = true
expectSuccess = true
followRedirects = true
- Started repro on upgrade from 3.3.1 to 3.3.3.
- Platform: Android
- Engine: OkHttp
RateLimit: Milliseconds in the Retry-After header are truncated
When a client is rate-limited, the server sends a Retry-After header with the number of seconds left until the client should send a request again:
internal var modifyResponse: (ApplicationCall, RateLimiter.State) -> Unit = { call, state ->
when (state) {
is RateLimiter.State.Available -> { /* ... */ }
is RateLimiter.State.Exhausted -> {
if (!call.response.headers.contains(HttpHeaders.RetryAfter)) {
call.response.header(HttpHeaders.RetryAfter, state.toWait.inWholeSeconds.toString())
}
}
}
}
But doing state.toWait.inWholeSeconds truncates any extra milliseconds. If the client waits exactly the number of seconds given, there's a high chance the request would fail with 429 again.
To fix this issue, the amount should always be rounded up.
Compiler Plugin
OpenAPI: No respective formats detected for serializable types like UUID or Instant
@Serializable
data class TestResponseDto(
@Serializable(UUIDAsStringSerializer::class) val uuid: UUID,
val id: Long,
@Serializable(InstantAsStringSerializer::class) val created: Instant,
@Serializable(InstantAsStringSerializer::class) val updated: Instant,
val name: String?
)
open-api.json:
"TestResponseDto": {
"type": "object",
"properties": {
"uuid": {
"type": "object",
"properties": {
"mostSigBits": {
"type": "integer"
},
"leastSigBits": {
"type": "integer"
}
}
},
"id": {
"type": "integer"
},
"created": {
"type": "object",
"properties": {
"seconds": {
"type": "integer"
},
"nanos": {
"type": "integer"
}
}
},
"updated": {
"type": "object",
"properties": {
"seconds": {
"type": "integer"
},
"nanos": {
"type": "integer"
}
}
},
"name": {
"type": "string"
}
}
},
Expected:
TestResponseDto:
type: "object"
properties:
uuid:
type: "string"
format: "uuid"
id:
type: "integer"
format: "int64"
created:
type: "string"
format: "date-time"
updated:
type: "string"
format: "date-time"
name:
type: "string"
nullable: true
required:
- "uuid"
- "id"
- "created"
- "updated"
I use
id("io.ktor.plugin") version "3.3.1"
To generate openApi spec
OpenAPI: Resource routes are missing inferred and comment-based documentation
When using type-safe resources, the generated documentation no longer appears to be complete in 3.4.0. The requestBody and responses do not appear to get added, and any documentation added through comments is missing.
Examples below, I have tried several variations without success.
Code
(io.ktor.server.resources.)post<UsersResource> {
val body = call.receive<CreateUserRequest>()
call.respond(
UserResponse(
id = 1,
name = body.name
)
)
}
Docs
"/users": {
"summary": null,
"description": null,
...
"post": {
...
"requestBody": null,
...
"responses": null,
},
...
},
Changing it to a "regular" route fixes the output:
Code
/**
* Endpoint for adding users.
*/
(io.ktor.server.routing.)post("/users") {
val body = call.receive<CreateUserRequest>()
call.respond(
UserResponse(
id = 1,
name = body.name
)
)
}
Docs
"/users": {
"summary": "Endpoint for adding users.",
"description": null,
...
"post": {
...
"requestBody": {
"description": null,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateUserRequest"
},
"examples": null,
"encoding": null
}
},
"required": false
},
...
"responses": {
"default": null,
"200": {
"description": "",
"headers": null,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserResponse"
},
"examples": null,
"encoding": null
}
},
"links": null
}
},
},
...
},
Attached is an example of what doesn't work.
OpenAPI: handle atypical route functions
We currently support the usual method functions: get, post, delete - but we should be able to provide support to any functions that accept a RouteHandler argument and return a Route.
OpenApi code inference misses lambda argument bodies
The return codes and types of the following code are evaluated correctly, open api shows a string for 200, an empty response for 403.
import io.ktor.http.HttpStatusCode
import io.ktor.openapi.OpenApiInfo
import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationCall
import io.ktor.server.netty.EngineMain
import io.ktor.server.plugins.openapi.openAPI
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
import io.ktor.server.routing.get
import io.ktor.server.routing.openapi.OpenApiDocSource
import io.ktor.server.routing.routing
import io.ktor.server.routing.routingRoot
import io.ktor.server.auth.jwt.JWTPrincipal
import io.ktor.server.auth.principal
fun main(args: Array<String>) {
EngineMain.main(args)
}
fun Application.module(
) {
configureRouting()
}
fun Application.configureRouting() {
routing {
openAPI(path = "openapi") {
info = OpenApiInfo("My API", "1.0")
source = OpenApiDocSource.Routing {
routingRoot.descendants()
}
}
get("/list") {
val principal = call.principal<JWTPrincipal>()
if (principal == null || !listOf("Peter", "John").contains(principal.payload.getClaim("username").asString())) {
call.respond(HttpStatusCode.Forbidden)
}
call.respond("hello")
}
}
}
After Extracting the auth check, the open api shows only an empty response for 403. 200 is missed completely.
import io.ktor.http.HttpStatusCode
import io.ktor.openapi.OpenApiInfo
import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationCall
import io.ktor.server.netty.EngineMain
import io.ktor.server.plugins.openapi.openAPI
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
import io.ktor.server.routing.get
import io.ktor.server.routing.openapi.OpenApiDocSource
import io.ktor.server.routing.routing
import io.ktor.server.routing.routingRoot
import io.ktor.server.auth.jwt.JWTPrincipal
import io.ktor.server.auth.principal
fun main(args: Array<String>) {
EngineMain.main(args)
}
fun Application.module(
) {
configureRouting()
}
fun Application.configureRouting() {
routing {
openAPI(path = "openapi") {
info = OpenApiInfo("My API", "1.0")
source = OpenApiDocSource.Routing {
routingRoot.descendants()
}
}
get("/list") {
checkAuthorization(listOf("Peter", "John")){
call.respond("hello")
}
}
}
}
suspend fun RoutingContext.checkAuthorization(
users: List<String>,
block: suspend ApplicationCall.(String) -> Unit
) {
val principal = call.principal<JWTPrincipal>()
if (principal == null || !users.contains(principal.payload.getClaim("username").asString())) {
call.respond(HttpStatusCode.Forbidden)
return
}
call.block(principal.payload.getClaim("username").asString())
}
For both examples the following build.gradle was used.
plugins {
kotlin("jvm") version "2.3.0"
id("io.ktor.plugin") version "3.4.0"
}
group = "org.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
application {
mainClass = "io.ktor.server.netty.EngineMain"
}
ktor {
openApi {
enabled = true
codeInferenceEnabled = true
onlyCommented = false
}
}
dependencies {
implementation("io.ktor:ktor-server-core")
implementation("io.ktor:ktor-server-netty")
implementation("io.ktor:ktor-server-openapi")
implementation("io.ktor:ktor-server-auth")
implementation("io.ktor:ktor-server-auth-jwt")
implementation("io.ktor:ktor-server-routing-openapi")
}
kotlin {
jvmToolchain(21)
}
Support "operationId" in Kdoc for OpenAPI spec. gen.
We already support a number of tags (https://ktor.io/docs/openapi-spec-generation.html#supported-kdoc-fields), but after the first launch, our users requested one more: `operationId`.
Core
Frame.Text.readText() causes infinite loop and 100% CPU on Kotlin/Native when WebSocket frame data is malformed or connection drops unexpectedly
Summary
When using Ktor WebSocket client on Kotlin/Native (Linux), calling Frame.Text.readText() can cause the worker thread to enter an infinite loop, pegging CPU usage at 100% indefinitely. The process does not crash and no exception is thrown. After this occurs, the WebSocket reconnect logic is also blocked and never executes.
The problem is still reproducible on Ktor 3.3.3 targeting Kotlin/Native linuxX64.
Ktor Version: 3.3.3 Kotlin Version: 2.1.x Target Platform: Kotlin/Native — linuxX64 Affected Component: ktor-client-websockets, ktor-io (CharsetDecoder.decode)
Steps to Reproduce
- Connect to a WebSocket server using Ktor client on Kotlin/Native.
- Let the server disconnect unexpectedly (e.g. server crash, network interruption).
- Observe that the thread processing
Frame.Textframes does not terminate and CPU usage climbs to 100%.
Minimal code pattern that triggers the issue:
kotlin
client.ws(urlString = config.url) {
while (isActive) {
val frame = incoming.receiveCatching().getOrNull() ?: continue
when (frame) {
is Frame.Text -> app.dispatch(frame.readText()) // <-- triggers infinite loop
is Frame.Close -> break
else -> Unit
}
}
}
A full real-world example can be found here:
https://github.com/qingshu-ui/OneBotMultiplatform/commit/9a80e42
Expected Behavior
Frame.Text.readText() should return the decoded string safely, or throw an exception if the stream is in an invalid state after disconnection.
Actual Behavior
The thread never returns from CharsetDecoder.decode(). CPU usage jumps to ~100% on that thread and never recovers. No exception is thrown and the coroutine never resumes.
Diagnostic Evidence
top -H output (thread-level CPU usage):
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
16 root 20 0 1308636 44700 2504 R 99.3 2.3 27:38.86 app.kexe
9 root 20 0 1308636 44700 2504 S 0.7 2.3 0:01.99 app.kexe
1 root 20 0 1308636 44700 2504 S 0.3 2.3 0:00.43 app.kexe
15 root 20 0 1308636 44700 2504 S 0.3 2.3 0:00.36 app.kexe
7 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.00 GC Timer thread
8 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.23 Main GC thread
10 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.00 app.kexe
11 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.39 app.kexe
12 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.36 app.kexe
13 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.40 app.kexe
14 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.00 app.kexe
17 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.37 app.kexe
18 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.37 app.kexe
19 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.38 app.kexe
20 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.36 app.kexe
21 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.34 app.kexe
22 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.38 app.kexe
23 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.42 app.kexe
24 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.34 app.kexe
25 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.19 app.kexe
1126 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.05 app.kexe
1127 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.06 app.kexe
1128 root 20 0 1308636 44700 2504 S 0.0 2.3 0:00.05 app.kexe
LLDB thread list — tid=16 mapped to lldb thread #11:
(lldb) thread list
Process 1 stopped
* thread #1: tid = 1, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #2: tid = 7, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'GC Timer thread', stop reason = signal SIGSTOP
thread #3: tid = 8, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'Main GC thread', stop reason = signal SIGSTOP
thread #4: tid = 9, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #5: tid = 10, 0x00007f306d31b832 libc.so.6`pselect + 210, name = 'app.kexe', stop reason = signal SIGSTOP
thread #6: tid = 11, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #7: tid = 12, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #8: tid = 13, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #9: tid = 14, 0x00007f306d31b832 libc.so.6`pselect + 210, name = 'app.kexe', stop reason = signal SIGSTOP
thread #10: tid = 15, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #11: tid = 16, 0x000000000056bbf0 app.kexe`kfun:io.ktor.utils.io.charsets#decode__at__io.ktor.utils.io.charsets.CharsetDecoder(kotlinx.io.Source;kotlin.Int){}kotlin.String + 1648, name = 'app.kexe', stop reason = signal SIGSTOP
thread #12: tid = 17, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #13: tid = 18, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #14: tid = 19, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #15: tid = 20, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #16: tid = 21, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #17: tid = 22, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #18: tid = 23, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #19: tid = 24, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #20: tid = 25, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #21: tid = 1126, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #22: tid = 1127, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #23: tid = 1128, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #24: tid = 1129, 0x00007f306d31b832 libc.so.6`pselect + 210, name = 'app.kexe', stop reason = signal SIGSTOP
thread #25: tid = 1130, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #26: tid = 1131, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #27: tid = 1132, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #28: tid = 1133, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #29: tid = 1134, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #30: tid = 1135, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #31: tid = 1136, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #32: tid = 1137, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
thread #33: tid = 1138, 0x00007f306d291117 libc.so.6`___lldb_unnamed_symbol3457 + 231, name = 'app.kexe', stop reason = signal SIGSTOP
LLDB bt of thread #11 (the hot thread):
(lldb) thread select 11
* thread #11, name = 'app.kexe', stop reason = signal SIGSTOP
frame #0: 0x000000000056bbf0 app.kexe`kfun:io.ktor.utils.io.charsets#decode__at__io.ktor.utils.io.charsets.CharsetDecoder(kotlinx.io.Source;kotlin.Int){}kotlin.String + 1648
app.kexe`kfun:io.ktor.utils.io.charsets#decode__at__io.ktor.utils.io.charsets.CharsetDecoder(kotlinx.io.Source;kotlin.Int){}kotlin.String:
-> 0x56bbf0 <+1648>: movq 0xe0(%rsp), %rdi
0x56bbf8 <+1656>: movq (%rdi), %rax
0x56bbfb <+1659>: andq $-0x4, %rax
0x56bbff <+1663>: movq (%rax), %rax
(lldb) bt
* thread #11, name = 'app.kexe', stop reason = signal SIGSTOP
* frame #0: 0x000000000056bbf0 app.kexe`kfun:io.ktor.utils.io.charsets#decode__at__io.ktor.utils.io.charsets.CharsetDecoder(kotlinx.io.Source;kotlin.Int){}kotlin.String + 1648
frame #1: 0x000000000073a05f app.kexe`kfun:io.github.qingshu_ui.onebot.ktor.$internalStartBotApplicationCOROUTINE$0.invokeSuspend#internal + 3631
frame #2: 0x0000000000469430 app.kexe`kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 224
frame #3: 0x0000000000538ddf app.kexe`kfun:kotlinx.coroutines.internal.ScopeCoroutine#afterResume(kotlin.Any?){} + 159
frame #4: 0x00000000004fe4db app.kexe`kfun:kotlinx.coroutines.AbstractCoroutine#resumeWith(kotlin.Result<1:0>){} + 187
frame #5: 0x0000000000469556 app.kexe`kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 518
frame #6: 0x0000000000535977 app.kexe`kfun:kotlinx.coroutines.DispatchedTask#run(){} + 759
frame #7: 0x0000000000537155 app.kexe`kfun:kotlinx.coroutines.internal.LimitedDispatcher.Worker.run#internal + 197
frame #8: 0x00000000005480c9 app.kexe`kfun:kotlinx.coroutines.MultiWorkerDispatcher.MultiWorkerDispatcher$workerRunLoop$1.$invokeCOROUTINE$0.invokeSuspend#internal + 1657
frame #9: 0x0000000000469430 app.kexe`kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 224
frame #10: 0x0000000000535977 app.kexe`kfun:kotlinx.coroutines.DispatchedTask#run(){} + 759
frame #11: 0x0000000000509d77 app.kexe`kfun:kotlinx.coroutines.EventLoopImplBase#processNextEvent(){}kotlin.Long + 967
frame #12: 0x0000000000541724 app.kexe`kfun:kotlinx.coroutines#runBlocking(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>){0§<kotlin.Any?>}0:0 + 1812
frame #13: 0x0000000000547917 app.kexe`kfun:kotlinx.coroutines.MultiWorkerDispatcher.MultiWorkerDispatcher$1.MultiWorkerDispatcher$1$invoke$$inlined$apply$1.$<bridge-DN>invoke(){}#internal + 215
frame #14: 0x000000000084d136 app.kexe`Worker::processQueueElement(bool) + 1590
frame #15: 0x000000000084ca6d app.kexe`(anonymous namespace)::workerRoutine(void*) + 109
frame #16: 0x00007f306d294ac3 libc.so.6`___lldb_unnamed_symbol3481 + 755
frame #17: 0x00007f306d325a84 libc.so.6`__clone + 68
Workaround
Replace frame.readText() with frame.data.decodeToString():
kotlin
// Before (causes infinite loop on Native):
is Frame.Text -> app.dispatch(frame.readText())
// After (safe: operates on an already-complete ByteArray in memory):
is Frame.Text -> app.dispatch(frame.data.decodeToString())
The fix has been applied in this commit: https://github.com/qingshu-ui/OneBotMultiplatform/commit/9a80e42
Related Issues
- KTOR-2829 — webSocketSession hangs forever in 2.0.0 (closed)
https://youtrack.jetbrains.com/issue/KTOR-2829
GMTDate: reduce allocations
Profiling shows GregorianCalendar + Gregorian$Date allocations from GMTDate construction account for ~864 samples per profiling run. Every HTTP response with a "Date" header triggers this allocation.
Replace Calendar.getInstance() / GregorianCalendar with a direct arithmetic algorithm (civil_from_days) that computes year/month/day from epoch milliseconds without allocating any objects.
String.decodeBase64String fails to decode when the input has no padding since 3.4.0
https://github.com/ktorio/ktor/issues/5336
Use Base64.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL) and likewise on the URL-encoded alternative to not break existing applications.
Docs
ContentEncoding: describe the `mode` configuration property
When trying to enable compression for a client request, you read this documentation:
https://ktor.io/docs/client-content-encoding.html#encode_request_body
Turns out, without changing the mode on the HttpClient to ContentEncodingConfig.Mode.All, even the compress(...) call in the HttpRequestBuilder has no effect.
This piece of information is missing in the documentation.
install(ContentEncoding) {
mode = ContentEncodingConfig.Mode.All
gzip()
}
Update library references in documentation
About a decade ago, we introduced some new artifacts for a few of our HTTP engines, but missed updating our documentation. The old artifacts are all EoL and should not be used.
Here are the artifacts that were renamed:
| old artifact | new artifact |
|---|---|
| ktor-server-jetty | ktor-server-jetty-jakarta |
| ktor-server-servlet | ktor-server-servlet-jakarta |
| ktor-server-tomcat | ktor-server-tomcat-jakarta |
We'll deprecate the old engines in 3.0 and point the project generator to the new artifacts as well.
Note: this should apply for 2.* documentation as well.
Add configuration example for jackson serializer
Geoffrey Vincent says about Content negotiation and serialization (version 2.2.3):
You don't explain how to configure jackson,
"The jackson method also allows you to adjust serialization settings provided by ObjectMapper."
But HOW do you write it down with your barely understandable syntax ????!!!!
Typo in parameter name in "Create a website"
In https://ktor.io/docs/server-create-website.html#reuse-routes the parameter taskName is mentioned instead of name*
Emmanuel* says about Create a website (version 2.3.12):
you use "taskName" as expected parameter name for your route "/byName", whereas in the form of your index page, to "View a task by name", you submit a parameter named "name".
Server
Routing: TailcardSelector missing toString(), which clutters the logs
Not entirely sure what it is or does, but in the CallLogging logs I can see it. Feels like I shouldn't, or at least without a memory address and a fully qualified name.
2026-02-21T01:26:16,027 TRACE [DefaultDispatcher-worker-3 @request#8] io.ktor.server.routing.Routing(RoutingRoot.kt:50) Trace for [anything]
/, segment:0 -> SUCCESS @ /
/ [(authenticate "default")], segment:0 -> SUCCESS @ / [(authenticate "default")]
/account [(authenticate "default")], segment:0 -> FAILURE "Selector didn't match" @ /account [(authenticate "default")]
/login, segment:0 -> FAILURE "Selector didn't match" @ /login
/ [(authenticate "default")], segment:0 -> SUCCESS @ / [(authenticate "default")]
/logout [(authenticate "default")], segment:0 -> FAILURE "Selector didn't match" @ /logout [(authenticate "default")]
/ [(authenticate "default")], segment:0 -> SUCCESS @ / [(authenticate "default")]
/auth [(authenticate "default")], segment:0 -> FAILURE "Selector didn't match" @ /auth [(authenticate "default")]
/ [(authenticate "default")], segment:0 -> SUCCESS @ / [(authenticate "default")]
/auth [(authenticate "default")], segment:0 -> FAILURE "Selector didn't match" @ /auth [(authenticate "default")]
/favicon.ico, segment:0 -> FAILURE "Selector didn't match" @ /favicon.ico
/ [io.ktor.server.http.content.TailcardSelector@7f8b4a5e], segment:0 -> SUCCESS @ / [io.ktor.server.http.content.TailcardSelector@7f8b4a5e]
/ [io.ktor.server.http.content.TailcardSelector@7f8b4a5e, <slash>], segment:0 -> SUCCESS @ / [io.ktor.server.http.content.TailcardSelector@7f8b4a5e, <slash>]
/{...} [io.ktor.server.http.content.TailcardSelector@7f8b4a5e, <slash>], segment:1 -> SUCCESS; Parameters [static-content-path-parameter=[anything]] @ /{...} [io.ktor.server.http.content.TailcardSelector@7f8b4a5e, <slash>]
/{...} [io.ktor.server.http.content.TailcardSelector@7f8b4a5e, <slash>, (method:GET)], segment:1 -> SUCCESS @ /{...} [io.ktor.server.http.content.TailcardSelector@7f8b4a5e, <slash>, (method:GET)]
/cinema, segment:0 -> FAILURE "Selector didn't match" @ /cinema
/film, segment:0 -> FAILURE "Selector didn't match" @ /film
/performance, segment:0 -> FAILURE "Selector didn't match" @ /performance
Matched routes:
"" -> "io.ktor.server.http.content.TailcardSelector@7f8b4a5e" -> "<slash>" -> "{...}" -> "(method:GET)"
based on a little trial and error (and confirmation from source code: private fun Route.staticContentRoute) it seems to be added by:
application.routing {
staticFiles("/", File(...)) {
}
}
Authentication: Creating JWT verifier fails for JWK with `kty=EC` and `alg=null`
Creating a JWT verifierer from a JWK, fails if the JWK has type (kty) EC and no algorithm (i.e. no alg, thus algorithm = null).
Currently, when building a JWT verifierer from a JWK with null algorithm, it simply assumes that the key type (kty) is RSA, which obviously breaks with other types. This was initially implemented in the this issue following Springs implementation, but even there it checks the key type first.
internal fun Jwk.makeAlgorithm(): Algorithm = when (algorithm) {
"RS256" -> Algorithm.RSA256(publicKey as RSAPublicKey, null)
"RS384" -> Algorithm.RSA384(publicKey as RSAPublicKey, null)
"RS512" -> Algorithm.RSA512(publicKey as RSAPublicKey, null)
"ES256" -> Algorithm.ECDSA256(publicKey as ECPublicKey, null)
"ES384" -> Algorithm.ECDSA384(publicKey as ECPublicKey, null)
"ES512" -> Algorithm.ECDSA512(publicKey as ECPublicKey, null)
null -> if (type == "EC") Algorithm.ECDSA256(publicKey as ECPublicKey, null)
else Algorithm.RSA256(publicKey as RSAPublicKey, null)
else -> throw IllegalArgumentException("Unsupported algorithm $algorithm")
}
This would also be fixed by this HUB-12541. Although I am unsure if it's still relevant.
OpenAPI: "No mapping for symbol: VAR FOR_LOOP_VARIABLE" error with codeInferenceEnabled=true
To reproduce, start the server with the following route and OpenAPI generator enabled where codeInferenceEnabled = true:
get("/cookies/set") {
for (n in call.queryParameters.names()) {
call.response.cookies.append(
name = n,
value = call.queryParameters[n] ?: "",
path = "/",
)
}
}
As a result, the compiler crashes with the following exception:
java.lang.RuntimeException: Exception while generating code for:
FUN name:mod visibility:public modality:FINAL returnType:kotlin.Unit
VALUE_PARAMETER kind:ExtensionReceiver name:<this> index:0 type:io.ktor.server.application.Application
VALUE_PARAMETER kind:Regular name:random index:1 type:kotlin.random.Random
BLOCK_BODY
COMPOSITE type=kotlin.Unit origin=null
CALL 'public final fun routing (<this>: io.ktor.server.application.Application, configuration: @[ExtensionFunctionType] kotlin.Function1<io.ktor.server.routing.Routing, kotlin.Unit>): io.ktor.server.routing.RoutingRoot declared in io.ktor.server.routing.RoutingRootKt' type=io.ktor.server.routing.RoutingRoot origin=null
ARG <this>: GET_VAR '<this>: io.ktor.server.application.Application declared in <root>.ServerKt.mod' type=io.ktor.server.application.Application origin=IMPLICIT_ARGUMENT
ARG configuration: BLOCK type=@[ExtensionFunctionType] kotlin.Function1<io.ktor.server.routing.Routing, kotlin.Unit> origin=LAMBDA
COMPOSITE type=kotlin.Unit origin=null
CALL 'public final fun <jvm-indy> <T> (dynamicCall: T of kotlin.jvm.internal.<jvm-indy>, bootstrapMethodHandle: kotlin.Any, vararg bootstrapMethodArguments: kotlin.Any): T of kotlin.jvm.internal.<jvm-indy> declared in kotlin.jvm.internal' type=@[ExtensionFunctionType] kotlin.Function1<io.ktor.server.routing.Routing, kotlin.Unit> origin=null
TYPE_ARG T: @[ExtensionFunctionType] kotlin.Function1<io.ktor.server.routing.Routing, kotlin.Unit>
ARG dynamicCall: CALL 'public final fun invoke (p0: kotlin.String): @[ExtensionFunctionType] kotlin.Function1<io.ktor.server.routing.Routing, kotlin.Unit> declared in kotlin.jvm.internal.invokeDynamic' type=@[ExtensionFunctionType] kotlin.Function1<io.ktor.server.routing.Routing, kotlin.Unit> origin=null
ARG p0: GET_VAR 'val n: kotlin.String [val] declared in <root>.ServerKt.mod$lambda$0.<no name provided>.invokeSuspend' type=kotlin.String origin=null
ARG bootstrapMethodHandle: CALL 'public final fun <jvm-method-handle> (tag: kotlin.Int, owner: kotlin.String, name: kotlin.String, descriptor: kotlin.String, isInterface: kotlin.Boolean): kotlin.Any declared in kotlin.jvm.internal' type=kotlin.Any origin=null
ARG tag: CONST Int type=kotlin.Int value=6
ARG owner: CONST String type=kotlin.String value="java/lang/invoke/LambdaMetafactory"
ARG name: CONST String type=kotlin.String value="metafactory"
ARG descriptor: CONST String type=kotlin.String value="(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;"
ARG isInterface: CONST Boolean type=kotlin.Boolean value=false
ARG bootstrapMethodArguments: VARARG type=kotlin.Array<kotlin.Any> varargElementType=kotlin.Any
CALL 'public final fun <jvm-original-method-type> (method: kotlin.Any): kotlin.Any declared in kotlin.jvm.internal' type=kotlin.Any origin=null
ARG method: RAW_FUNCTION_REFERENCE 'public abstract fun invoke (p1: P1 of kotlin.Function1): R of kotlin.Function1 [operator] declared in kotlin.Function1' type=kotlin.Any
RAW_FUNCTION_REFERENCE 'private final fun mod$lambda$0 ($n: kotlin.String, $this$routing: io.ktor.server.routing.Routing): kotlin.Unit? declared in <root>.ServerKt' type=kotlin.Any
CALL 'public final fun <jvm-original-method-type> (method: kotlin.Any): kotlin.Any declared in kotlin.jvm.internal' type=kotlin.Any origin=null
ARG method: RAW_FUNCTION_REFERENCE 'public abstract fun invoke (p1: io.ktor.server.routing.Routing): kotlin.Unit? [fake_override,operator] declared in kotlin.jvm.internal.invokeDynamic.<fake>' type=kotlin.Any
COMPOSITE type=kotlin.Unit origin=null
at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate(FunctionCodegen.kt:56)
at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate$default(FunctionCodegen.kt:49)
at org.jetbrains.kotlin.backend.jvm.codegen.ClassCodegen.generateMethodNode(ClassCodegen.kt:428)
at org.jetbrains.kotlin.backend.jvm.codegen.ClassCodegen.generateMethod(ClassCodegen.kt:445)
at org.jetbrains.kotlin.backend.jvm.codegen.ClassCodegen.generate(ClassCodegen.kt:168)
at org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory.generateFile(JvmIrCodegenFactory.kt:467)
at org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory.invokeCodegen(JvmIrCodegenFactory.kt:433)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runCodegen$cli(KotlinToJVMBytecodeCompiler.kt:409)
at org.jetbrains.kotlin.cli.pipeline.jvm.JvmBackendPipelinePhase.executePhase(JvmBackendPipelinePhase.kt:89)
at org.jetbrains.kotlin.cli.pipeline.jvm.JvmBackendPipelinePhase.executePhase(JvmBackendPipelinePhase.kt:27)
at org.jetbrains.kotlin.cli.pipeline.PipelinePhase.phaseBody(PipelinePhase.kt:68)
at org.jetbrains.kotlin.cli.pipeline.PipelinePhase.phaseBody(PipelinePhase.kt:58)
at org.jetbrains.kotlin.config.phaser.NamedCompilerPhase.invoke(CompilerPhase.kt:102)
at org.jetbrains.kotlin.backend.common.phaser.CompositePhase.invoke(PhaseBuilders.kt:22)
at org.jetbrains.kotlin.config.phaser.CompilerPhaseKt.invokeToplevel(CompilerPhase.kt:53)
at org.jetbrains.kotlin.cli.pipeline.AbstractCliPipeline.runPhasedPipeline(AbstractCliPipeline.kt:109)
at org.jetbrains.kotlin.cli.pipeline.AbstractCliPipeline.execute(AbstractCliPipeline.kt:68)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecutePhased(K2JVMCompiler.kt:51)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecutePhased(K2JVMCompiler.kt:42)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:89)
at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.kt:359)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunnerBase.runCompiler(IncrementalJvmCompilerRunnerBase.kt:178)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunnerBase.runCompiler(IncrementalJvmCompilerRunnerBase.kt:40)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:504)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:418)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally$lambda$0$compile(IncrementalCompilerRunner.kt:249)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally(IncrementalCompilerRunner.kt:267)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:119)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.lang.IllegalStateException: No mapping for symbol: VAR FOR_LOOP_VARIABLE name:n type:kotlin.String [val]
at org.jetbrains.kotlin.backend.jvm.codegen.IrFrameMap.typeOf(irCodegenUtils.kt:59)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitGetValue(ExpressionCodegen.kt:706)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitGetValue(ExpressionCodegen.kt:138)
at org.jetbrains.kotlin.ir.expressions.IrGetValue.accept(IrGetValue.kt:18)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.gen(ExpressionCodegen.kt:212)
at org.jetbrains.kotlin.backend.jvm.codegen.IrCallGenerator.genValueAndPut(IrCallGenerator.kt:48)
at org.jetbrains.kotlin.backend.jvm.intrinsics.JvmInvokeDynamic.invoke(JvmInvokeDynamic.kt:53)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitCall(ExpressionCodegen.kt:510)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitCall(ExpressionCodegen.kt:138)
at org.jetbrains.kotlin.ir.expressions.IrCall.accept(IrCall.kt:24)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitStatementContainer(ExpressionCodegen.kt:489)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitContainerExpression(ExpressionCodegen.kt:503)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitContainerExpression(ExpressionCodegen.kt:138)
at org.jetbrains.kotlin.ir.visitors.IrVisitor.visitBlock(IrVisitor.kt:122)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitBlock(ExpressionCodegen.kt:427)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitBlock(ExpressionCodegen.kt:138)
at org.jetbrains.kotlin.ir.expressions.IrBlock.accept(IrBlock.kt:18)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.gen(ExpressionCodegen.kt:212)
at org.jetbrains.kotlin.backend.jvm.codegen.IrCallGenerator.genValueAndPut(IrCallGenerator.kt:48)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitCall$handleParameter(ExpressionCodegen.kt:526)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitCall(ExpressionCodegen.kt:539)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitCall(ExpressionCodegen.kt:138)
at org.jetbrains.kotlin.ir.expressions.IrCall.accept(IrCall.kt:24)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitStatementContainer(ExpressionCodegen.kt:489)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitContainerExpression(ExpressionCodegen.kt:503)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitContainerExpression(ExpressionCodegen.kt:138)
at org.jetbrains.kotlin.ir.visitors.IrVisitor.visitComposite(IrVisitor.kt:125)
at org.jetbrains.kotlin.ir.expressions.IrComposite.accept(IrComposite.kt:18)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitStatementContainer(ExpressionCodegen.kt:489)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitBlockBody(ExpressionCodegen.kt:494)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitBlockBody(ExpressionCodegen.kt:138)
at org.jetbrains.kotlin.ir.expressions.IrBlockBody.accept(IrBlockBody.kt:20)
at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.generate(ExpressionCodegen.kt:231)
at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.doGenerate(FunctionCodegen.kt:132)
at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate(FunctionCodegen.kt:53)
... 45 more
OpenAPI: UnsupportedOperationException for a function with a reified type parameter codeInferenceEnabled = true
OpenAPI generation crashes withUnsupportedOperationException if codeInferenceEnabled is true, and an inline function with a reified type parameter is used.
Exception in thread "main" java.lang.UnsupportedOperationException: This function has a reified type parameter and thus can only be inlined at compilation time, not called directly.
at kotlin.jvm.internal.Intrinsics.throwUndefinedForReified(Intrinsics.java:209)
at kotlin.jvm.internal.Intrinsics.throwUndefinedForReified(Intrinsics.java:203)
at kotlin.jvm.internal.Intrinsics.reifiedOperationMarker(Intrinsics.java:213)
at my.class$lambda$0$0$0$0$0$0$0(ClassUsingFunctionWithReifiedType.kt:1)
at io.ktor.openapi.Responses$Builder.response(Operation.kt:733)
Coroutines in route handlers are dispatched with Dispatchers.Unconfined since 3.2.0
To reproduce, start the following server and hit the /test endpoint:
val threadFactory = ThreadFactory { r ->
Thread(r, "My thread")
}
val dispatcher = Executors.newCachedThreadPool(threadFactory).asCoroutineDispatcher()
fun main() {
embeddedServer(Netty, 8083) {
routing {
get("/test") {
println(Thread.currentThread())
withContext(dispatcher) {
delay(1000)
}
println(Thread.currentThread()) // Switched to "My thread"
}
}
}.start(wait = true)
}
As a result, the rest of the code after the dispatcher switch executes on a thread backed by the dispatcher:
Thread[#48,eventLoopGroupProxy-4-2,5,main]
Thread[#49,My thread,5,main]
OpenAPI: Cannot override kotlinx.serialization module
There's currently no way to use custom descriptors for schema inference. This can block the use of the OpenAPI describe API.
OpenAPI: Order of path parameters is not preserved in the spec
To reproduce, generate spec for the following route:
get("/test/{param1}/{param2}") {
call.respond(HttpStatusCode.OK)
}
As a result, the following JSON schema is generated:
[
{
"name": "param2",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "param1",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
]
The problem is that the ordering of the parameters in the path are not preserved in the spec.
It's not a bug, since the OpenAPI specification doesn't impose an ordering requirement, but the order should be preserved to avoid confusion in the UI.
JSON schema inference does not recognize unsigned types
When building schema for a UInt, for example, the output resembles this:
unsignedInt:
type: object
title: kotlin.UInt
required:
- '[UNINITIALIZED]'
properties:
'[UNINITIALIZED]':
type: integer
OpenAPI describe needs defaults
Currently, SwaggerUI looks errored out when nothing is provided for an operation. When expanding, you get an infinite spinner.
To solve this, we just need to provide an empty summary or something.
OpenAPI: jsonSchema<T>() does not unwrap Kotlin value classes (inline classes)
jsonSchema<T>() generates object schemas for @JvmInline value class types instead of unwrapping to the underlying type's schema.
Minimal reproduction
@Serializable
@JvmInline
value class SKU(val value: String)
@Serializable
data class ProductDTO(val sku: SKU, val name: String)
// In route:
get { /* ... */ }.describe {
responses {
HttpStatusCode.OK {
schema = jsonSchema<ProductDTO>()
}
}
}
Actual schema generated
{
"sku": { "type": "object", "properties": { "value": { "type": "string" } } },
"name": { "type": "string" }
}
Expected schema
{
"sku": { "type": "string" },
"name": { "type": "string" }
}
The actual API response serializes SKU as a plain string (kotlinx-serialization handles value classes by unwrapping them), so the schema documentation doesn't match the actual JSON payload.
Root cause
KotlinxJsonSchemaInference.buildJsonSchema() dispatches on SerialDescriptor.kind but doesn't check SerialDescriptor.isInline. Value classes hit the StructureKind.CLASS branch and get expanded as objects.
Suggested fix
Add an isInline check at the top of buildJsonSchema():
if (isInline) {
return getElementDescriptor(0).buildJsonSchema(includeTitle, visiting)
}
Related
- KTOR-9004 (schema generator not recognizing serializable types like UUID/Instant)
Environment
- Ktor 3.4.0
- Kotlin 2.3.0
- kotlinx-serialization 1.8.1
OpenAPI static content path appears in resulting model
When using the OpenAPI plugin, the /openapi/{**} path appears in the resulting routes.
This should be hidden automatically.
JWT: Docs for `validate` method claim that it's optional, but it isn't
The documentation (https://ktor.io/docs/server-jwt.html#validate-payload) mentions:
The
validatefunction allows you to perform additional validations on the JWT payload
However, this function is not optional but mandatory for authentication to work. If no validation is configured, the validate The function raises an exception to the API implementation by default. It fails authentication without raising an evident exception for the developer, but only a trace log about authentication itself failing.
The API in question:
{width=70%}
The default implementation of the function:
{width=70%}
How it is used:
{width=70%}
Two things can be done. Either reword the documentation and convey that it is mandatory or raise an exception for the developer at instantiation time. Alternatively, as the documentation suggests, this function should be optional and not mandatory in the first place to pass authentication.
Incorrect dependency declaration in swagger / openapi
Users are currently forced to include the ktor-server-routing-openapi whenever they use OpenAPI or Swagger plugins.
We should fix this by changing the dependency from implementation to api
Shared
WebSockets: Infinite spin and potential OOM vulnerabilities in the Inflater.inflateFully method
Currently, KTOR websocket code has a critical security issue, allowing an attacker to send malicious payload that will cause OOM on the handler. Please let me know if I should provide more details publicly here or write me privately.
Test Infrastructure
testApplication: Race condition in timeout coroutine when response is streaming
To reproduce, execute these tests with Ktor 3.2.2. As a result, the ClosedWriteChannelException is thrown:
io.ktor.utils.io.ClosedWriteChannelException
at io.ktor.utils.io.ByteChannel.getWriteBuffer(ByteChannel.kt:55)
at io.ktor.utils.io.CountedByteWriteChannel.getWriteBuffer(CountedByteWriteChannel.kt:27)
at io.ktor.utils.io.CountedByteWriteChannel.getTotalBytesWritten(CountedByteWriteChannel.kt:18)
at io.ktor.server.testing.TestApplicationResponse$respondWriteChannelContent$writerJob$1$killJob$1.invokeSuspend(TestApplicationResponse.kt:123)
at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:42)
at kotlinx.coroutines.flow.FlowKt__CollectionKt.toCollection(Collection.kt:22)
at ai.grazie.tasks.alias.CompletionAliasTasksTest$test fallback to ALL model kotlin$1$1$1.invokeSuspend(CompletionAliasTasksTest.kt:73)
at ai.grazie.tasks.alias.CompletionAliasTasksTest$test fallback to ALL model kotlin$1$1.invokeSuspend(CompletionAliasTasksTest.kt:63)
at ai.grazie.cloud.testing.AuthContextsKt$withServiceAuthContext$2.invokeSuspend(AuthContexts.kt:58)
at ai.grazie.cloud.testing.AuthContextsKt$withAuthContext$2.invokeSuspend(AuthContexts.kt:82)
at ai.grazie.tasks.alias.CompletionAliasTasksTest$test fallback to ALL model kotlin$1.invokeSuspend(CompletionAliasTasksTest.kt:62)
at ai.grazie.tasks.fixtures.GrazieTasksServiceWithMocksFixture$taskService$2.invokeSuspend(GrazieTasksServiceWithMocksFixture.kt:58)
at ai.grazie.cloud.testing.UtilsKt$grazieTestApplication$1$1.invokeSuspend(Utils.kt:81)
at io.ktor.server.testing.TestApplicationKt$runTestApplication$2$2.invokeSuspend(TestApplication.kt:534)
at io.ktor.server.testing.TestApplicationKt.runTestApplication(TestApplication.kt:534)
at ai.grazie.cloud.testing.UtilsKt$grazieTestApplication$1.invokeSuspend(Utils.kt:65)
at io.ktor.test.dispatcher.TestCommonKt$runTestWithRealTime$1.invokeSuspend(TestCommon.kt:40)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$1.invokeSuspend(TestBuilders.kt:317)
Suppressed: io.ktor.utils.io.ClosedWriteChannelException
at io.ktor.utils.io.ByteChannel.getWriteBuffer(ByteChannel.kt:55)
at io.ktor.utils.io.CountedByteWriteChannel.getWriteBuffer(CountedByteWriteChannel.kt:27)
at io.ktor.utils.io.CountedByteWriteChannel.getTotalBytesWritten(CountedByteWriteChannel.kt:18)
at io.ktor.server.testing.TestApplicationResponse$respondWriteChannelContent$writerJob$1$killJob$1.invokeSuspend(TestApplicationResponse.kt:123)
The problem is reproducible only when the entire class is executed, not individual tests.
Other
HttpProtocolVersion.parse: fast path for common versions
HttpProtocolVersion.parse() allocates a List and iterator via split() on every call, even for the four common HTTP version strings. Profiling shows ~530 allocation samples from this path.
Add equality checks for "HTTP/1.0", "HTTP/1.1", "HTTP/2.0", and "HTTP/3.0" before falling through to the split()-based parsing, returning cached HttpProtocolVersion constants for the common case and completely avoiding allocation.
HTMX: "on" attributes extension not working
Using "on" attributes like this:
attributes.hx {
post = "/torrents/$category"
swap = "none"
on("before-request", "var b=this.querySelector('button[type=submit]'); b.dataset.originalHtml=b.dataset.originalHtml||b.innerHTML; b.innerHTML='Saving...'; b.disabled=true;")
on("after-request", "var b=this.querySelector('button[type=submit]'); if(event.detail.successful){b.innerHTML='Saved ✅'; b.disabled=true;}")
on("response-error", "var b=this.querySelector('button[type=submit]'); b.innerHTML=b.dataset.originalHtml||b.innerHTML; b.disabled=false;")
}
This results in attributes like hx-on:before-request when it should instead be hx-on::before-request as per the documentation.
3.4.0
released 23rd January 2026
Client
Java: Use HTTP/2 by default
Java HttpClient uses HTTP/2 by default with fallback to HTTP/1.1
Ktor changes the default protocol version to HTTP/1.1
Run HttpStatement.execute on the engine's dispatcher
We provide the following example in docs for reading streaming responses:
client.prepareGet("https://httpbin.org/bytes/$fileSize").execute { httpResponse ->
val channel: ByteReadChannel = httpResponse.body()
var count = 0L
stream.use {
while (!channel.exhausted()) {
val chunk = channel.readRemaining(bufferSize)
count += chunk.remaining
chunk.transferTo(stream)
println("Received $count bytes from ${httpResponse.contentLength()}")
}
}
}
This code assumes that the block inside execute is executed with the engine's dispatcher. In fact, it uses the caller's coroutine context.
Even though network calls are performed in the response coroutine context, this code performs writing into a file on the caller's thread.
Curl: HttpResponse.version always returns HTTP_1_1
... as we don't read the actual protocol version here
Auth/Bearer: Make BearerAuthProvider detect disguised Bearer scheme
Multiple major cloud providers do not return accurate WWW-Authenticate headers. For example, ServiceNow returns Basic and Salesforce returns Token, even though both accept Bearer. This causes Ktor Client to not call the configured refreshToken handler, as it only applies when WWW-Authenticate returns "Bearer".
Curl: `caPath` is not set by default in the Curl client on linuxArm64
The following trivial code passes on Linux X64, but fails on Linux Arm64 with a SSL error:
val client = HttpClient(Curl) { }
val res = client.get("https://kotlinlang.org/")
println("status=${res.status} bytes=${res.bodyAsBytes().size}")
// X64: status=200 OK bytes=115427
// Arm64: SSL peer certificate or SSH remote key was not OK
client.close()
It can be worked around by manually setting `caPath`, but this creates unnecessary boilerplate in multiplatform projects because the client configuration becomes platform specific.
val client = HttpClient(Curl) {
engine {
caPath = "/etc/ssl/certs"
}
}
The root cause is that CURLINFO_CAPATH is set to `/etc/ssl/certs` in the `libcurl.a` compiled for X64, but set to null in the `libcurl.a` compiled for ARM64. Those libs, last updated in https://github.com/ktorio/ktor/pull/4445, must have been built in an inconsistent manner. This can be verified with:
#include <stdio.h>
#include "curl.h"
int main(void)
{
CURL *curl = curl_easy_init();
if(curl) {
char *capath = NULL;
curl_easy_getinfo(curl, CURLINFO_CAPATH, &capath);
if(capath) {
printf("default ca path: %s\n", capath);
}
curl_easy_cleanup(curl);
}
}
Maybe we could at least set `caPath = "/etc/ssl/certs"` by default, which is a sane default and should work out of the box for most distribs.
Deprecate DarwinLegacy engine
This engine is needed only to support iOS 12 or lower.
After removing the engine, it should also be removed from test filters.
WebRTC. `IceServer.urls` should be a list.
Atm, `IceServer.urls` is just a string. As a result, the browser can't reuse common credentials, which degrades performance.
Curl always uses HTTP/1.1
It seems that libcurl has been compiled without HTTP2 support.
@Test
fun printCurlFeatures() {
val versionInfo = curl_version_info(CURLversion.byValue(CURLVERSION_NOW.toUInt()))
if (versionInfo != null) {
val info = versionInfo.pointed
println("Supported features:")
var index = 0
while (true) {
val feature = info.feature_names?.get(index) ?: break
println(" - ${feature.toKString()}")
index++
}
}
}
Supported features:
- alt-svc
- AsynchDNS
- HSTS
- HTTPS-proxy
- IPv6
- Largefile
- libz
- NTLM
- SSL
- threadsafe
- UnixSockets
SSE: Java engine does not close the underlying connection when SSE session is canceled
If you attempt to connect to an SSE endpoint using Ktor (Java Client) + SSE and then attempt to close the SSE session, the underlying SSE connection will NOT close on the backend. This bug does NOT happen when using the CIO client.
suspend fun main() {
val http = HttpClient(Java) {
this.expectSuccess = false
this.followRedirects = false
install(SSE)
}
val sseSession = http.sseSession("http://127.0.0.1:13004/sse") {}
sseSession.cancel() // this should cancel the SSE connection BUT, if you look at the backend, the connection will still be active
delay(100_000) // delay to demonstrate the issue
}
Auth: Provide control over tokens to user code
Currently, tokens (or credentials) are cached in AuthTokenHolder (which is internal).
This behavior is unobvious to users and leads to several issues:
- KTOR-4759 Auth: BearerAuthProvider caches result of loadToken until process death
- KTOR-6569 Bearer auth: Don't cache client bearer token (option)
- KTOR-4946 Auth: Bearer authentication - unable to update tokens
- KTOR-7775 Auth: BasicAuthProvider caches credentials until process death
Users have to use workarounds to clear cache and update credentials:
- KTOR-7884 Auth: The MutableList cannot be accessed since 3.0.0
We provide a method clearToken as a temporary solution, but it can't be the final API to solve the problem, as it is error-prone. Users have to clear tokens twice: in their token holder implementation and in the auth provider.
If we provide control over AuthTokenHolder (or other API in replacement) to user code, we:
- make it clear that it is the responsibility of calling code to clear tokens
- allow users to provide non-caching implementation
Previous discussion: https://github.com/ktorio/ktor/pull/4645#discussion_r1939692688
Auth: The MutableList<AuthProvider> cannot be accessed since 3.0.0
After upgrading to Ktor Client 3.x, I'm not able to update the basic auth credentials of the user after HTTPClient initialization anymore. In case the user changed the password, I did the following with Ktor Client 2.3.x:
authWebservice.httpClient.plugin(Auth).providers.clear()
authWebservice.httpClient.plugin(Auth).basic {
sendWithoutRequest { true }
credentials {
BasicAuthCredentials(
username = username,
password = newPassword
)
}
}
I didn't find any API in 3.x to update the credentials or re-install the auth plugin. Do I miss something?
Thanks a lot.
Bearer auth: Don't cache client bearer token (option)
In our case we already cache the token from firebase auth. (Which is why we don't pass a refreshToken)
loadTokens {
auth.bearerToken().getOrNull()?.let { BearerTokens(accessToken = it, refreshToken = "") }
}
Firebase will auto refresh the token behind the scenes so we can just fetch directly from our cache instead of the plugin holding it. This also removes the cases where we have to wait for a 401 to come back and then call "Refresh" to firebase which already has a new token wasting an already avoidable API request.
The MAIN reason we don't want caching is that there is that when a client logs out we have to do this jumping through hoops to remove the cached token, or if we want to forcibly refresh the token ahead of time.
client.plugin(Auth).providers.filterIsInstance<BearerAuthProvider>().forEach { it.clearToken() }
Arguably we could (should) create an authed DI scope for authed vs unauthed, but there are other cases when we just want to provide a token that we already have.
Cheers!
Auth: Bearer authentication - unable to update tokens
Hi there,
I've followed to guide in order to implement bearer authentication and noticed a use case that has not been handled.
Specifically the loadTokens method is only called once, on the first request, and never again.
Consider this example, which is exactly what's mentioned in the oauth google example in your docs:
- first request is unauthenticated, used to actually fetch auth tokens.
loadTokensis called and it will cache the fact that there are no tokens yet - the second request should use the tokens that have been fetched earlier, but
loadTokensis not called and this request will fail with a 401 status code. At this point the refresh mechanism will be triggered and peace will be restored - now if user decides to logout, again, we should have a way to tell the oauth engine to clear the cached tokens
The only way I was able to resolve these issues is to manually call the clearToken method inside BearerAuthProvider: is it really the only workaround?
Thanks!
Auth: BearerAuthProvider caches result of loadToken until process death
In applications which use Ktor Client & allow their users to log out (which effectively means removing their tokens from storage), Ktor will continue to use the previous token for future requests because BearerAuthProvider uses AuthTokenHolder which caches the result of the provided loadTokens function until manually evicted. The documentation for Bearer Auth makes no mention of this cache policy nor does it explain how clear tokens from AuthTokenHolder.
Given the following code:
val tokenStorage = mutableListOf(BearerTokens("access_token", "refresh_token"))
val mockEngine = MockEngine { request ->
respondOk("""{ "status": "ok" } """)
}
val client = HttpClient(mockEngine) {
install(Auth) {
bearer {
loadTokens {
tokenStorage.firstOrNull()
}
}
}
}
println("Starting request one...")
client.get("localhost").bodyAsText()
println("Expected Token: ${tokenStorage.firstOrNull()?.accessToken}")
println("Actual Token: ${mockEngine.requestHistory[0].headers[HttpHeaders.Authorization]?.removePrefix("Bearer ")}")
println("Clearing token storage...")
tokenStorage.clear()
// Undocumented work-around:
//client.pluginOrNull(Auth)?.providers?.filterIsInstance<BearerAuthProvider>()?.forEach {
// it.clearToken()
//}
println("Starting request two...")
client.get("localhost").bodyAsText()
println("Expected Token: ${tokenStorage.firstOrNull()?.accessToken}")
println("Actual Token: ${mockEngine.requestHistory[1].headers[HttpHeaders.Authorization]?.removePrefix("Bearer ")}")
will print:
Starting request one...
Expected Token: access_token
Actual Token: access_token
Clearing token storage...
Starting request two...
Expected Token: null
Actual Token: access_token
I've included a workaround as a comment in the above code block on how to manually evict the in-memory cache of AuthTokenHolder.
Recommended Solution: Expose a clearToken() function from the context of BearerTokenConfig so callers can easily clear the in-memory cache when they externally clear their token storage. Update the documentation to make this clearer by invoking clearToken() in the example.
Alternate Solution: Remove the in-memory cache AuthTokenHolder all together & always read directly from the source. However, this will reduce per-request performance if the source is disk-based.
Related: KTOR-3593
Fix SendCountExceedException when maxRetries = Int.MAX_VALUE
iOS native interop for WebRTC client
Add an iOS target for the WebRTC client on top of WebRTC-SDK cocoapod, which also works for macOS. First, we should test the iOS version, adding macOS support later.
Bearer Auth: request cancellation causes refresh token invalidation
Here is a flow describing the issue:
- Client has a pair of access and refresh tokens in the storage
- The access token is expired
- Client sends a request to a protected resource
- This triggers invocation of the
refreshTokensblock because the server responded with 401 (the token has expired) - Client cancels the request's scope, causing the authorization server to send the new access/refresh tokens pair and invalidate the sent refresh token. However, the token storage isn't updated because of the cancellation and contains the invalidated refresh token.
- From now on, the token cannot be refreshed, so the client has to re-login.
To reproduce the problem, run the following test:
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.mock.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.utils.io.*
import kotlinx.coroutines.*
import kotlinx.serialization.Required
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
enum class TokenTypeNetworkModel {
BEARER
}
class KtorTest {
@ExperimentalUuidApi
@Test
fun recreateThatKtorUsesWrongTokensWhenCancellationsHappenWhileRefreshingTokens(): Unit = runBlocking {
// The below test simulates an API that supports Bearer tokens, and that the token is refreshed when it expires, and refreshes are triggered when endpoints return 401 responses.
@Serializable
data class TokensNetworkModel(
@SerialName(value = "access_token") @Required val accessToken: String,
@SerialName(value = "expires_in") @Required val expiresIn: Int,
@SerialName(value = "refresh_token") @Required val refreshToken: String,
@SerialName(value = "refresh_token_expires_in") @Required val refreshTokenExpiresIn: Int,
@SerialName(value = "token_type") @Required val tokenType: TokenTypeNetworkModel
)
val tokens = (1..1000).map {
TokensNetworkModel(
accessToken = Uuid.random().toString(),
expiresIn = 1.seconds.inWholeSeconds.toInt(),
refreshToken = Uuid.random().toString(),
refreshTokenExpiresIn = 28.days.inWholeSeconds.toInt(),
tokenType = TokenTypeNetworkModel.BEARER,
)
}
var activeTokenIndex = 0
var isActiveAccessTokenExpired = false
var accessTokenExpirationJob: Job? = null
fun startAccessTokenExpirationJob(tokensNetworkModel: TokensNetworkModel) = launch {
isActiveAccessTokenExpired = false
delay(tokensNetworkModel.expiresIn.seconds)
isActiveAccessTokenExpired = true
}
// Start initial token expiration.
accessTokenExpirationJob = startAccessTokenExpirationJob(tokens[activeTokenIndex])
val mockEngine = MockEngine { request ->
// If token is valid.
if (request.url.encodedPath.contains("auth/token") && request.url.parameters["grant_type"] == "refresh_token" && request.url.parameters["refresh_token"] == tokens[activeTokenIndex].refreshToken) {
// We got a refresh token request and the refresh token is valid. Turn to the next token and return that one.
activeTokenIndex += 1
val newToken = tokens[activeTokenIndex]
// Start a job that will render the accessToken expired after newToken.expiresIn seconds.
accessTokenExpirationJob?.cancel()
accessTokenExpirationJob = startAccessTokenExpirationJob(newToken)
println("Token refreshed to: ${newToken.accessToken}")
// Allow a little time to make cancellation happen before response is returned.
delay(500.milliseconds)
// Respond with a 200 OK
respond(
content = ByteReadChannel("""{"access_token":"${newToken.accessToken}","expires_in":${newToken.expiresIn},"refresh_token":"${newToken.refreshToken}","refresh_token_expires_in":${newToken.refreshTokenExpiresIn},"token_type":"Bearer"}"""),
status = HttpStatusCode.OK,
headers = headersOf(HttpHeaders.ContentType, "application/json")
)
}
// If refresh token is invalid.
else if (request.url.encodedPath.contains("auth/token") && request.url.parameters["grant_type"] == "refresh_token" && request.url.parameters["refresh_token"] != tokens[activeTokenIndex].refreshToken) {
// We got a refresh token request but the refresh token is invalid. Respond with a 401 Unauthorized.
respond(
content = ByteReadChannel("""{"error":"Refresh failed","error_description":"Invalid refresh token"}"""),
status = HttpStatusCode.Unauthorized,
headers = headersOf(HttpHeaders.ContentType, "application/json")
)
}
// If token is valid and not expired.
else if (request.url.encodedPath.contains("get/something") && request.headers["Authorization"] == "Bearer ${tokens[activeTokenIndex].accessToken}" && !isActiveAccessTokenExpired) {
// We got a request that requires a token, and the token is valid. Respond with a 200 OK.
respondOk("""{"data":"some data"}""")
}
// If token is expired or wrong token is supplied.
else if (request.url.encodedPath.contains("get/something") && (request.headers["Authorization"] != "Bearer ${tokens[activeTokenIndex].accessToken}" || isActiveAccessTokenExpired)) {
// We got a request that requires a token, but the token is invalid. Respond with a 401 Unauthorized.
respond(
content = ByteReadChannel(""),
status = HttpStatusCode.Unauthorized,
headers = headersOf(HttpHeaders.ContentType, "application/json")
)
}
// Internal server error.
else {
println("Internal server error. Returned 400.")
respondBadRequest()
}
}
val client = HttpClient(mockEngine) {
var tokenStorage: BearerTokens =
tokens[activeTokenIndex].let { BearerTokens(it.accessToken, it.refreshToken) }
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true // Allows for unquoted strings and quoted booleans etc.e
coerceInputValues =
true // Allows for values to be set to the default when json input cannot be parsed.
})
}
install(Auth) {
bearer {
sendWithoutRequest { request ->
!(request.url.encodedPath.contains("auth/token"))
}
loadTokens {
println("LoadTokens called.")
// Return the current token from storage.
tokenStorage
}
refreshTokens {
println("RefreshTokens called.")
// Refresh the token and return the new token.
val tokenParams = this
val refreshToken = tokenParams.oldTokens!!.refreshToken
val newTokens =
tokenParams.client.post(Url("/auth/token?grant_type=refresh_token&refresh_token=$refreshToken")) {
markAsRefreshTokenRequest()
}.also {
// Log the raw response
println("Response body: " + it.bodyAsText())
}.also {
// Parse the response body to a TokensNetworkModel
if (it.status == HttpStatusCode.Unauthorized) {
throw Exception("You were logged out due to sending a an invalid refresh token")
}
}.body<TokensNetworkModel>()
val result = BearerTokens(newTokens.accessToken, newTokens.refreshToken)
tokenStorage = result
println("Updated token in storage to: ${result.accessToken}")
result
}
}
}
}
// Wait until the first token expires.
delay(tokens[activeTokenIndex].expiresIn.seconds)
// Make a get something call, this will trigger a refresh token call as access token is expired.
val getSomethingJob = async { client.post(urlString = "/get/something") }
// Wait for the refresh token call to internally generate a new token and invalidate the old one.
delay(250.milliseconds)
// Cancel the coroutineScope, this will cancel the refresh token call, and render us in a state where the token in BE is new, but the one in the Ktor cache is old an expired.
getSomethingJob.cancel()
launch { client.post(urlString = "/get/something") }
}
}
As a result, the server responds with 401 because the refresh token is no longer valid.
Client/WebSocket/Darwin close code and reason are incorrect
When reading DefaultWebSocketSession.closeReason.code on ktor-client-darwin, I am always getting 1000 ("normal") even though the server closed with a different code. The message is always empty even though the server closed with a specific message.
I am not getting this issue on the jvm with ktor-client-okhttp.
Here's a reproducer: (project is here)
class MyTest {
private fun Application.webSocketServer() {
install(WebSockets)
routing {
webSocket("/") {
close(CloseReason(code = 4242, message = "Bye"))
}
}
}
@Test
fun test() = runTest {
println("Hello, world!")
val port = Random.nextInt(10000, 20000)
val server = embeddedServer(CIO, port) { webSocketServer() }.start(wait = false)
val client = HttpClient {
install(io.ktor.client.plugins.websocket.WebSockets)
}
client.webSocket("ws://127.0.0.1:$port") {
println("Connected")
try {
incoming.receive()
} catch (e: Exception) {
println("WebSocket was closed code=${closeReason.await()?.code} reason=${closeReason.await()?.message}")
server.stop(0, 0)
}
}
}
}
git clone https://github.com/BoD/issue-ktor-ws-close-code-reason
cd issue-ktor-ws-close-code-reason
./gradlew allTest
Expected:
WebSocket was closed code=4242 reason=Bye
Actual:
WebSocket was closed code=1000 reason=
headers { } block does not affect the request in `defaultRequest` due to function name collision with io.ktor.http.headers
Description
When using the headers { } block inside defaultRequest, headers are silently not applied to requests. This happens because the compiler resolves to io.ktor.http.headers (top-level function) instead of io.ktor.client.request.headers (extension function).
Reproduction
Code that does NOT work:
val client = HttpClient(CIO) {
defaultRequest {
url("https://api.example.com")
headers {
append(HttpHeaders.Authorization, "Bearer my-secret-token")
append(HttpHeaders.Accept, ContentType.Application.Json.toString())
}
}
}
// Authorization header is NOT included in the request
client.get("/endpoint")
Code that DOES work:
val client = HttpClient(CIO) {
defaultRequest {
url("https://api.example.com")
headers.append(HttpHeaders.Authorization, "Bearer my-secret-token")
headers.append(HttpHeaders.Accept, ContentType.Application.Json.toString())
}
}
// Authorization header IS included
client.get("/endpoint")
Root Cause Analysis
There are two functions with the same name headers
| Package | Signature | Behavior |
|---|---|---|
io.ktor.http |
fun headers(builder: HeadersBuilder.() -\> Unit): Headers |
Creates and returns a new Headers object |
io.ktor.client.request |
fun HttpMessageBuilder.headers(block: HeadersBuilder.() -\> Unit): HeadersBuilder |
Modifies existing HeadersBuilder in place |
Why it fails in DefaultRequestBuilder
In DefaultRequest.kt
// DefaultRequest.kt (package io.ktor.client.plugins)
import io.ktor.client.request.*
import io.ktor.http.*
public class DefaultRequestBuilder internal constructor() : HttpMessageBuilder {
override val headers: HeadersBuilder = HeadersBuilder()
// ...
}
When calling headers { } inside defaultRequest block:
- Compiler sees two candidates: top-level
io.ktor.http.headersand extensionio.ktor.client.request.headers - Top-level function takes precedence over extension function in this context
io.ktor.http.headerscreates a newHeadersobject, but the result is discarded- The actual
DefaultRequestBuilder.headersproperty remains unchanged
Why it works in HttpRequestBuilder
In HttpRequest.kt:
// HttpRequest.kt (package io.ktor.client.request)
public class HttpRequestBuilder : HttpMessageBuilder { ... }
// Same file, same package
public fun HttpMessageBuilder.headers(block: HeadersBuilder.() -> Unit): HeadersBuilder =
headers.apply(block)
Since HttpRequestBuilder and the headers extension function are in the same file and package, the extension function is correctly resolved.
Impact
- Silent failure: No compile-time error, no runtime exception
- Difficult to debug: Headers appear to be set but are never sent
- Inconsistent behavior: Same code works in
HttpRequestBuilderbut not inDefaultRequestBuilder - Common use case affected: Setting default Authorization headers is a very common pattern
Suggested Solutions
Option 1: Add headers member function to DefaultRequestBuilder (Recommended)
public class DefaultRequestBuilder internal constructor() : HttpMessageBuilder {
override val headers: HeadersBuilder = HeadersBuilder()
// Add this member function to shadow the top-level function
public fun headers(block: HeadersBuilder.() -> Unit): HeadersBuilder =
headers.apply(block)
// ...
}
Option 2: Rename io.ktor.http.headers to avoid collision
// Instead of:
public fun headers(builder: HeadersBuilder.() -> Unit): Headers
// Use:
public fun buildHeaders(builder: HeadersBuilder.() -> Unit): Headers
Note: This would be a breaking change.
Option 3: Add documentation warning
At minimum, add a warning in the defaultRequest documentation about this behavior.
Related Code References
io.ktor.http.headers: Headers.kt:95io.ktor.client.request.headers: HttpRequest.kt:294DefaultRequestBuilder: DefaultRequest.kt:167
HttpCookies: Support parsing non-compilant `Expires` dates of Set-Cookie header
I'm developing an unofficial API library and cannot control the server's response format.
The server returns a Set-Cookie header with a date value that causes a parsing error in Ktor's HttpCookies plugin.
The error occurs when the plugin tries to parse the cookie's expiration date. The server returns "Wed" as part of the date string, which is not a valid format for String.fromHttpToGmtDate() parsing, resulting in an IllegalStateException.
My ktor http client config
val httpClient = HttpClient {
install(ContentNegotiation) {
json(json)
}
install(DefaultRequest) {
contentType(ContentType.Application.Json)
}
install(HttpCookies) { // Because of that
}
expectSuccess = true
}
The solution use runCatching to ignore the error
return Cookie(
name = first.key,
value = decodeCookieValue(first.value, encoding),
encoding = encoding,
maxAge = loweredMap["max-age"]?.toIntClamping(),
expires = runCatching { loweredMap["expires"]?.fromCookieToGmtDate() }.getOrNull(),
domain = loweredMap["domain"],
path = loweredMap["path"],
secure = "secure" in loweredMap,
httpOnly = "httponly" in loweredMap,
extensions = asMap.filterKeys {
it.toLowerCasePreservingASCIIRules() !in loweredPartNames && it != first.key
}
)
Log
[DefaultDispatcher-worker-4] java.lang.IllegalStateException: Failed to parse date: Wed
at io.ktor.http.DateUtilsKt.fromHttpToGmtDate(DateUtils.kt:40)
at io.ktor.http.DateUtilsKt.fromCookieToGmtDate(DateUtils.kt:58)
at io.ktor.http.CookieKt.parseServerSetCookieHeader(Cookie.kt:111)
at io.ktor.http.HttpMessagePropertiesKt.setCookie(HttpMessageProperties.kt:122)
at io.ktor.client.plugins.cookies.HttpCookies.saveCookiesFrom$ktor_client_core(HttpCookies.kt:83)
at io.ktor.client.plugins.cookies.HttpCookies$Companion$install$3.invokeSuspend(HttpCookies.kt:136)
at io.ktor.client.plugins.cookies.HttpCookies$Companion$install$3.invoke(Unknown Source:11)
at io.ktor.client.plugins.cookies.HttpCookies$Companion$install$3.invoke(Unknown Source:6)
at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:79)
at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57)
at io.ktor.util.pipeline.DebugPipelineContext.proceedWith(DebugPipelineContext.kt:42)
at io.ktor.client.plugins.DoubleReceivePluginKt$SaveBody$1$1.invokeSuspend(SaveBody.kt:52)
at io.ktor.client.plugins.DoubleReceivePluginKt$SaveBody$1$1.invoke(Unknown Source:11)
at io.ktor.client.plugins.DoubleReceivePluginKt$SaveBody$1$1.invoke(Unknown Source:6)
at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:79)
at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57)
at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63)
at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:92)
at io.ktor.client.HttpClient$2.invokeSuspend(HttpClient.kt:1367)
at io.ktor.client.HttpClient$2.invoke(Unknown Source:13)
at io.ktor.client.HttpClient$2.invoke(Unknown Source:4)
at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:79)
at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57)
at io.ktor.util.pipeline.DebugPipelineContext.proceedWith(DebugPipelineContext.kt:42)
at io.ktor.client.engine.HttpClientEngine$install$1.invokeSuspend(HttpClientEngine.kt:166)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:124)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:89)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:820)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
Update libcurl to 8.18.0
Binaries versions:
| Library | Version | Comment |
|---|---|---|
| libcurl | 8.18.0 | |
| openssl | 3.6.0 | Linux and macOS only |
| libnghttp2 | 1.68.0 | |
| zlib | 1.3.1 | Windows only |
macOS:
Secure Transport support has been dropped in curl 8.15.0 , so now it uses OpenSSL for TLS connection similarly to as on Linux
Logging: Body logging of multipart/form-data requests hangs when OkHttp format is on
When uploading attachments via multipart InputProvider wrapping a single-use InputStream, the request hangs for files slightly over 1MB with Ktor logging enabled at LogLevel.ALL (OkHttp
format). Packet capture shows no request being sent. Commenting out logging makes the upload succeed.
Root Cause
Ktor 3.1.3 Logging plugin reads request bodies by duplicating the outbound ByteReadChannel via split(client) in Logging.kt. For OutgoingContent.ReadChannelContent:
- split writes each chunk to two ByteChannels (origChannel for sending, newChannel for logging) and awaits both writes (awaitAll) before continuing.
- ByteChannel has a 1MB total buffer (CHANNEL_MAX_SIZE = 1024*1024). If a channel isn’t consumed, flush() suspends at sleepWhile { flushBufferSize >= CHANNEL_MAX_SIZE }.
- In OkHttp format, detectIfBinary reads 1KB to classify the body, but split continues to feed the logging channel even though it’s no longer being read. For a ~1.1MB body, the logging channel fills to 1MB, writeFully/flush
suspends, awaitAll blocks, and split stops producing. The send pipeline never proceeds to the engine, so the request is never sent.
Native engines should use Dispatchers.IO not Dispatchers.Unconfined
The Darwin engine uses Dispatchers.Unconfined which cannot be changed. Should it use Dispatchers.IO?
The same applies to WinHttp and Curl engines.
Apache5: Simplify configuration of ConnectionManager
For various purposes, we use clients with different timeouts, and we set up those timeouts with HttpTimeout plugin. And because Apache5 has moved MaxConnTotal and MaxConnPerRoute from one builder to another, we may no longer rely on this plugin.
Could you please provide some kind of a Ktor builder to set up settings like MaxConnTotal and MaxConnPerRoute for Apache5, so that it would be possible to tweak it with the plugins?
For reference, here are our setups for Apache5, Apache and CIO:
// HttpTimeout doesn't work
HttpClient(Apache5) {
engine {
customizeClient {
setConnectionManager(
PoolingAsyncClientConnectionManagerBuilder.create()
.setMaxConnTotal(10_000)
.setMaxConnPerRoute(1_000)
.build()
)
}
}
}
// HttpTimeout plugin works
HttpClient(Apache) {
engine {
customizeClient {
setMaxConnTotal(10_000)
setMaxConnPerRoute(1_000)
}
}
}
// HttpTimeout plugin works
HttpClient(CIO) {
engine {
endpoint {
maxConnectionsPerRoute = 1024
pipelineMaxSize = 100
}
}
}
DefaultRequest: Configuration applied twice for client created with `HttpClient.config`
When you create a new client based on previous client with the `config` method and you install new `defaultRequest` the old one keeps working. It does not replace the previous install. Instead they stack.
For example the below will print first log:... Bearer firstAuth by the first client and then second log: ... Bearer firstAuth by the second client instead of the expected second log: Bearer secondAuth
val client = HttpClient {
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
println("first log#####\n${message}")
}
}
level = LogLevel.HEADERS
}
defaultRequest {
headers.append(HttpHeaders.Authorization, "Bearer firstAuth")
}
}
runBlocking {
client.get("https://www.google.com")
}
val client2 = client.config {
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
println("second log#####\n${message}")
}
}
level = LogLevel.HEADERS
}
defaultRequest {
// headers.append(HttpHeaders.Authorization, "Bearer secondAuth")
headers.appendIfNameAbsent(HttpHeaders.Authorization, "Bearer secondAuth")
}
}
runBlocking {
client2.get("https://www.google.com")
}
Multipart/form-data: Make `formData`'s block inline
Franko Vlahov says about Ktor documentation
Api symbol: io.ktor.client.request.forms.formData:
Make it inline so we can execute suspend functions inside (if the parent executing function is a suspend)
HttpCallValidatorConfig.handleResponseException() should receive a CallExceptionHandler
I think the correct interface for HttpCallValidatorConfig.handleResponseException() is CallExceptionHandler, not CallRequestExceptionHandler. If handleResponseException() receives CallRequestExceptionHandler, there is no difference between `handleResponseException()` and handleResponseExceptionWithRequest().
https://github.com/ktorio/ktor/blob/de26b15670df3c2ffe8ae1453df622c43ce71f97/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/HttpCallValidator.kt#L39
In previous versions, handleResponseException() used CallExceptionHandler, which only takes an Exception as an argument.
https://github.com/ktorio/ktor/pull/2862
Compiler Plugin
OpenAPI generation fails with unresolved reference errors due to whitespace changes when incremental compilation is enabled
Running the buildOpenApi ktor gradle task fails in some cases when whitespace near the imports or package declaration is added or removed, without running build clean between builds.
So running the buildOpenApi task succeeds on a fresh project; however, after changing some of the whitespace described below, it fails.
Running build clean inbetween the two builds prevents the issue.
For all failures some "Unresolved reference" errors are reported (see below).
Some examples I've encountered are:
Removing empty line between package and import statements
- This is observable with the openapi sample project from github.
- Removing the empty line between
package io.ktor.samples.openapiandimport io.ktor.server.application.*inApplication.ktleads to a compilation error
Adding empty line before imports for files without a package statement
- This is also observable with the sample project mentioned above
- When removing the package declaration from all files, such that the
importstatements start in the first line, clean compilation succeeds - When adding a single newline in front of the
import io.ktor.server.application.*line inApplication.kt, the build fails similarly to the failure described above
Adding empty line before package declaration
- I observed this in a personal project but was yet unable to reproduce with a smaller sample project
- I have several subfolders in
src/main/kotlin, e.g. "routes", which includes files that have the package declarationpackage routes - If those package declarations start in the first line, the build succeeds, however, if there is a newline before the package statement, it will fail, again with "unresolved reference" errors.
Used versions/tools:
- kotlin version: 2.2.20
- ktor version: 3.3.1 (for the sample project) and 3.3.0 (for the personal project)
- builds run via the IntelliJ gradle integration
- arch based linux using the "Eclipse Temurin 24.0.2" SDK
Error message:
Obtained using just buildOpenApi in the modified sample project, see the attached file for the full error message obtained via buildOpenApi --debug.
> Task :checkKotlinGradlePluginConfigurationErrors SKIPPED
> Task :buildOpenApi FAILED
Ktor's OpenAPI generation is ** experimental **
- It may be incompatible with Kotlin versions outside 2.2.20
- Behavior will likely change in future releases
- Please report any issues at https://youtrack.jetbrains.com/newIssue?project=KTOR
1 actionable task: 1 executed
e: file:///home/sev/projects/tmp/ktorio%20ktor-samples%20main%20openapi-(2)/src/main/kotlin/Application.kt:9:5 Unresolved reference 'configureOpenApi'.
e: file:///home/sev/projects/tmp/ktorio%20ktor-samples%20main%20openapi-(2)/src/main/kotlin/Application.kt:10:5 Unresolved reference 'configureSerialization'.
e: file:///home/sev/projects/tmp/ktorio%20ktor-samples%20main%20openapi-(2)/src/main/kotlin/Application.kt:11:5 Unresolved reference 'configureSecurity'.
e: file:///home/sev/projects/tmp/ktorio%20ktor-samples%20main%20openapi-(2)/src/main/kotlin/Application.kt:12:5 Unresolved reference 'configureRouting'.
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':buildOpenApi'.
> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
> Compilation error. See log for more details
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to generate a Build Scan (Powered by Develocity).
> Get more help at https://help.gradle.org.
BUILD FAILED in 120ms
OpenAPI: Broken schema generation for @response code [Type]
When annotating a route with @response code [Type] description, but the Type is neither received nor responded with using call.receive<[...]>() / call.respond<[...]>([...]) there is a problem with the schema generation.
If it is written as @response code [my/package/Foo] description the paths $ref will be "$ref": "#/components/schemas/my/package/Foo", but should be "$ref": "#/components/schemas/Foo". Otherwise the reference can't be resolved.
If instead it is written as @response code [Foo] description the $ref will be correct, but the schema for Foo is not generated at all.
I have added a Test case to demonstrate this issue here: https://github.com/floriantfuhrmann/ktor-build-plugins/commit/ef39e91a2eef10aa096d033743cd78855ea22754
Missing implementation of `getPluginId` method error with Kotlin 2.3.0-RC
The buildOpenApi task fails when using Kotlin 2.3.0-RC due to a binary compatibility issue in the compiler plugin. The build crashes with an AbstractMethodError because the plugin does not implement getPluginId(), which appears to be required by the updated CompilerPluginRegistrar in Kotlin 2.3.0. Other community plugins have resolved similar forward compatibility issues by explicitly exposing this method (https://github.com/drewhamilton/Poko/pull/628/files). Can you do the same thing for ktor until we have proper support for Kotlin 2.3.0? Everything else works fine with ktor 2.3.0-RC
e: java.lang.AbstractMethodError: Missing implementation of resolved method 'abstract java.lang.String getPluginId()' of abstract class org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar.
at org.jetbrains.kotlin.cli.jvm.plugins.PluginCliParser.loadPluginsLegacyStyle(PluginCliParser.kt:244)
at org.jetbrains.kotlin.cli.jvm.plugins.PluginCliParser.loadPluginsSafe(PluginCliParser.kt:112)
at org.jetbrains.kotlin.cli.pipeline.AbstractConfigurationPhase.loadCompilerPlugins(AbstractConfigurationPhase.kt:122)
at org.jetbrains.kotlin.cli.pipeline.AbstractConfigurationPhase.setupCommonConfiguration(AbstractConfigurationPhase.kt:71)
at org.jetbrains.kotlin.cli.pipeline.AbstractConfigurationPhase.executePhase(AbstractConfigurationPhase.kt:47)
https://youtrack.jetbrains.com/projects/KT/issues/KT-55300
https://youtrack.jetbrains.com/issue/KT-82507
OpenAPI: "AssertionError: Cannot add a performance measurements" leading to StackOverflowError within a multimodule project
Getting multiple errors when I try to build a project
Ktor's OpenAPI generation is ** experimental **
- It may be incompatible with Kotlin versions outside 2.2.20
- Behavior will likely change in future releases
- Please report any issues at https://youtrack.jetbrains.com/newIssue?project=KTOR
e: Daemon compilation failed: null
java.lang.Exception
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
at org.gradle.internal.Factories$1.create(Factories.java:31)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.lang.AssertionError: Cannot add a performance measurements because it's already finalized
at org.jetbrains.kotlin.com.intellij.openapi.diagnostic.DefaultLogger.error(DefaultLogger.java:83)
at org.jetbrains.kotlin.com.intellij.openapi.diagnostic.Logger.error(Logger.java:436)
at org.jetbrains.kotlin.com.intellij.openapi.util.ObjectTree.handleExceptions(ObjectTree.java:195)
at org.jetbrains.kotlin.com.intellij.openapi.util.ObjectTree.runWithTrace(ObjectTree.java:141)
at org.jetbrains.kotlin.com.intellij.openapi.util.ObjectTree.executeAll(ObjectTree.java:162)
at org.jetbrains.kotlin.com.intellij.openapi.util.Disposer.dispose(Disposer.java:205)
at org.jetbrains.kotlin.com.intellij.openapi.util.Disposer.dispose(Disposer.java:193)
at org.jetbrains.kotlin.cli.common.UtilsKt.disposeRootInWriteAction$lambda$0(utils.kt:142)
at org.jetbrains.kotlin.com.intellij.openapi.application.ActionsKt.runWriteAction$lambda$0(actions.kt:16)
at org.jetbrains.kotlin.com.intellij.mock.MockApplication.runWriteAction(MockApplication.java:209)
at org.jetbrains.kotlin.com.intellij.openapi.application.ActionsKt.runWriteAction(actions.kt:16)
at org.jetbrains.kotlin.cli.common.UtilsKt.disposeRootInWriteAction(utils.kt:141)
at org.jetbrains.kotlin.cli.pipeline.AbstractCliPipeline.execute(AbstractCliPipeline.kt:93)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecutePhased(K2JVMCompiler.kt:79)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecutePhased(K2JVMCompiler.kt:45)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:90)
at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.kt:352)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunnerBase.runCompiler(IncrementalJvmCompilerRunnerBase.kt:176)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunnerBase.runCompiler(IncrementalJvmCompilerRunnerBase.kt:39)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:499)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:416)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)
... 3 more
Caused by: java.lang.AssertionError: Cannot add a performance measurements because it's already finalized
at org.jetbrains.kotlin.util.PerformanceManager.ensureNotFinalizedAndSameThread(PerformanceManager.kt:382)
at org.jetbrains.kotlin.util.PerformanceManager.measureSideTime$compiler_common(PerformanceManager.kt:305)
at org.jetbrains.kotlin.util.PerformanceManagerKt.tryMeasureSideTime(PerformanceManager.kt:406)
at org.jetbrains.kotlin.load.kotlin.VirtualFileKotlinClass$Factory.create$frontend_common_jvm(VirtualFileKotlinClass.kt:73)
at org.jetbrains.kotlin.load.kotlin.KotlinBinaryClassCache$Companion.getKotlinBinaryClassOrClassFileContent$lambda$0(KotlinBinaryClassCache.kt:98)
at org.jetbrains.kotlin.com.intellij.mock.MockApplication.runReadAction(MockApplication.java:194)
at org.jetbrains.kotlin.load.kotlin.KotlinBinaryClassCache$Companion.getKotlinBinaryClassOrClassFileContent(KotlinBinaryClassCache.kt:97)
at org.jetbrains.kotlin.load.kotlin.KotlinBinaryClassCache$Companion.getKotlinBinaryClassOrClassFileContent$default(KotlinBinaryClassCache.kt:78)
at org.jetbrains.kotlin.load.kotlin.VirtualFileFinder.findKotlinClassOrContent(VirtualFileFinder.kt:39)
at org.jetbrains.kotlin.fir.java.deserialization.JvmClassFileBasedSymbolProvider.extractClassMetadata(JvmClassFileBasedSymbolProvider.kt:171)
at org.jetbrains.kotlin.fir.deserialization.AbstractFirDeserializedSymbolProvider.findAndDeserializeClass(AbstractFirDeserializedSymbolProvider.kt:256)
at org.jetbrains.kotlin.fir.deserialization.AbstractFirDeserializedSymbolProvider.classCache$lambda$0(AbstractFirDeserializedSymbolProvider.kt:163)
at org.jetbrains.kotlin.fir.caches.FirThreadUnsafeCacheWithPostCompute.getValue(FirThreadUnsafeCachesFactory.kt:75)
at org.jetbrains.kotlin.fir.deserialization.AbstractFirDeserializedSymbolProvider.getClass(AbstractFirDeserializedSymbolProvider.kt:343)
at org.jetbrains.kotlin.fir.deserialization.AbstractFirDeserializedSymbolProvider.getClass$default(AbstractFirDeserializedSymbolProvider.kt:326)
at org.jetbrains.kotlin.fir.deserialization.AbstractFirDeserializedSymbolProvider.getClassLikeSymbolByClassId(AbstractFirDeserializedSymbolProvider.kt:420)
at org.jetbrains.kotlin.fir.resolve.providers.impl.FirCachingCompositeSymbolProvider.computeClass(FirCachingCompositeSymbolProvider.kt:147)
at org.jetbrains.kotlin.fir.resolve.providers.impl.FirCachingCompositeSymbolProvider.access$computeClass(FirCachingCompositeSymbolProvider.kt:27)
at org.jetbrains.kotlin.fir.resolve.providers.impl.FirCachingCompositeSymbolProvider$special$$inlined$createCache$1.invoke(FirCachesFactory.kt:163)
at org.jetbrains.kotlin.fir.resolve.providers.impl.FirCachingCompositeSymbolProvider$special$$inlined$createCache$1.invoke(FirCachesFactory.kt:147)
at org.jetbrains.kotlin.fir.caches.FirThreadUnsafeCache.getValue(FirThreadUnsafeCachesFactory.kt:57)
at org.jetbrains.kotlin.fir.resolve.providers.impl.FirCachingCompositeSymbolProvider.getClassLikeSymbolByClassId(FirCachingCompositeSymbolProvider.kt:174)
at org.jetbrains.kotlin.fir.resolve.ToSymbolUtilsKt.toSymbol(ToSymbolUtils.kt:57)
at org.jetbrains.kotlin.fir.resolve.ToSymbolUtilsKt.toRegularClassSymbol(ToSymbolUtils.kt:73)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinTypeForFlexibleBound(JavaTypeConversion.kt:240)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinTypeForFlexibleBound$default(JavaTypeConversion.kt:200)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeTypeProjection(JavaTypeConversion.kt:118)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinType(JavaTypeConversion.kt:91)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinType$default(JavaTypeConversion.kt:86)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeTypeProjection(JavaTypeConversion.kt:156)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinType(JavaTypeConversion.kt:91)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinType$default(JavaTypeConversion.kt:86)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toFirResolvedTypeRef(JavaTypeConversion.kt:79)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.resolveIfJavaType(JavaTypeConversion.kt:55)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinTypeProbablyFlexible(JavaTypeConversion.kt:64)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.toConeKotlinType(SignatureEnhancement.kt:1024)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhance(SignatureEnhancement.kt:1013)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhance(SignatureEnhancement.kt:1006)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhanceReturnType(SignatureEnhancement.kt:902)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhanceReturnType(SignatureEnhancement.kt:851)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhance$fir_jvm(SignatureEnhancement.kt:169)
at org.jetbrains.kotlin.fir.java.enhancement.FirEnhancedSymbolsStorage$EnhancementSymbolsCache.enhancedVariables$lambda$0(SignatureEnhancement.kt:1152)
at org.jetbrains.kotlin.fir.caches.FirThreadUnsafeCache.getValue(FirThreadUnsafeCachesFactory.kt:57)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhancedProperty(SignatureEnhancement.kt:137)
at org.jetbrains.kotlin.fir.java.scopes.JavaClassMembersEnhancementScope.processPropertiesByName$lambda$0(JavaClassMembersEnhancementScope.kt:33)
at org.jetbrains.kotlin.fir.scopes.impl.AbstractFirUseSiteMemberScope.processPropertiesByName(AbstractFirUseSiteMemberScope.kt:159)
at org.jetbrains.kotlin.fir.java.scopes.JavaClassMembersEnhancementScope.processPropertiesByName(JavaClassMembersEnhancementScope.kt:32)
at org.jetbrains.kotlin.fir.scopes.impl.FirScopeWithCallableCopyReturnTypeUpdater.processPropertiesByName(FirScopeWithCallableCopyReturnTypeUpdater.kt:40)
at org.jetbrains.kotlin.fir.scopes.FirContainingNamesAwareScopeKt.processAllProperties(FirContainingNamesAwareScope.kt:30)
at io.ktor.compiler.utils.PropertiesUtilsKt.getAllPropertiesFromType(PropertiesUtils.kt:42)
at io.ktor.openapi.model.JsonSchema$Companion.schemaDefinitionForType(JsonSchema.kt:96)
at io.ktor.openapi.model.JsonSchema$Companion.findSchemaDefinitions(JsonSchema.kt:50)
at io.ktor.openapi.routing.interpreters.CallRespondInterpreter.check$lambda$0(CallRespondInterpreter.kt:43)
at io.ktor.openapi.routing.RouteNode.resolve(RouteNode.kt:16)
at io.ktor.openapi.routing.RouteNode$CallFeature.resolve(RouteNode.kt:55)
at io.ktor.openapi.routing.RouteCollector.resolve(RouteCollector.kt:42)
at io.ktor.openapi.routing.RouteCollector.collectRoutes(RouteCollector.kt:17)
at io.ktor.openapi.OpenApiSpecGenerator.buildSpecification(OpenApiSpecGenerator.kt:17)
at io.ktor.openapi.OpenApiExtension.saveSpecification(OpenApiExtension.kt:59)
at io.ktor.compiler.KtorCompilerPluginRegistrar.registerExtensions$lambda$0(KtorCompilerPluginRegistrar.kt:27)
at org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrarKt.registerInProject$lambda$1(CompilerPluginRegistrar.kt:72)
at org.jetbrains.kotlin.com.intellij.openapi.util.ObjectTree.runWithTrace(ObjectTree.java:130)
... 37 more
Failed to compile with Kotlin daemon: java.lang.Exception
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
at org.gradle.internal.Factories$1.create(Factories.java:31)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.lang.AssertionError: Cannot add a performance measurements because it's already finalized
at org.jetbrains.kotlin.com.intellij.openapi.diagnostic.DefaultLogger.error(DefaultLogger.java:83)
at org.jetbrains.kotlin.com.intellij.openapi.diagnostic.Logger.error(Logger.java:436)
at org.jetbrains.kotlin.com.intellij.openapi.util.ObjectTree.handleExceptions(ObjectTree.java:195)
at org.jetbrains.kotlin.com.intellij.openapi.util.ObjectTree.runWithTrace(ObjectTree.java:141)
at org.jetbrains.kotlin.com.intellij.openapi.util.ObjectTree.executeAll(ObjectTree.java:162)
at org.jetbrains.kotlin.com.intellij.openapi.util.Disposer.dispose(Disposer.java:205)
at org.jetbrains.kotlin.com.intellij.openapi.util.Disposer.dispose(Disposer.java:193)
at org.jetbrains.kotlin.cli.common.UtilsKt.disposeRootInWriteAction$lambda$0(utils.kt:142)
at org.jetbrains.kotlin.com.intellij.openapi.application.ActionsKt.runWriteAction$lambda$0(actions.kt:16)
at org.jetbrains.kotlin.com.intellij.mock.MockApplication.runWriteAction(MockApplication.java:209)
at org.jetbrains.kotlin.com.intellij.openapi.application.ActionsKt.runWriteAction(actions.kt:16)
at org.jetbrains.kotlin.cli.common.UtilsKt.disposeRootInWriteAction(utils.kt:141)
at org.jetbrains.kotlin.cli.pipeline.AbstractCliPipeline.execute(AbstractCliPipeline.kt:93)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecutePhased(K2JVMCompiler.kt:79)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecutePhased(K2JVMCompiler.kt:45)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:90)
at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.kt:352)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunnerBase.runCompiler(IncrementalJvmCompilerRunnerBase.kt:176)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunnerBase.runCompiler(IncrementalJvmCompilerRunnerBase.kt:39)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:499)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:416)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)
... 3 more
Caused by: java.lang.AssertionError: Cannot add a performance measurements because it's already finalized
at org.jetbrains.kotlin.util.PerformanceManager.ensureNotFinalizedAndSameThread(PerformanceManager.kt:382)
at org.jetbrains.kotlin.util.PerformanceManager.measureSideTime$compiler_common(PerformanceManager.kt:305)
at org.jetbrains.kotlin.util.PerformanceManagerKt.tryMeasureSideTime(PerformanceManager.kt:406)
at org.jetbrains.kotlin.load.kotlin.VirtualFileKotlinClass$Factory.create$frontend_common_jvm(VirtualFileKotlinClass.kt:73)
at org.jetbrains.kotlin.load.kotlin.KotlinBinaryClassCache$Companion.getKotlinBinaryClassOrClassFileContent$lambda$0(KotlinBinaryClassCache.kt:98)
at org.jetbrains.kotlin.com.intellij.mock.MockApplication.runReadAction(MockApplication.java:194)
at org.jetbrains.kotlin.load.kotlin.KotlinBinaryClassCache$Companion.getKotlinBinaryClassOrClassFileContent(KotlinBinaryClassCache.kt:97)
at org.jetbrains.kotlin.load.kotlin.KotlinBinaryClassCache$Companion.getKotlinBinaryClassOrClassFileContent$default(KotlinBinaryClassCache.kt:78)
at org.jetbrains.kotlin.load.kotlin.VirtualFileFinder.findKotlinClassOrContent(VirtualFileFinder.kt:39)
at org.jetbrains.kotlin.fir.java.deserialization.JvmClassFileBasedSymbolProvider.extractClassMetadata(JvmClassFileBasedSymbolProvider.kt:171)
at org.jetbrains.kotlin.fir.deserialization.AbstractFirDeserializedSymbolProvider.findAndDeserializeClass(AbstractFirDeserializedSymbolProvider.kt:256)
at org.jetbrains.kotlin.fir.deserialization.AbstractFirDeserializedSymbolProvider.classCache$lambda$0(AbstractFirDeserializedSymbolProvider.kt:163)
at org.jetbrains.kotlin.fir.caches.FirThreadUnsafeCacheWithPostCompute.getValue(FirThreadUnsafeCachesFactory.kt:75)
at org.jetbrains.kotlin.fir.deserialization.AbstractFirDeserializedSymbolProvider.getClass(AbstractFirDeserializedSymbolProvider.kt:343)
at org.jetbrains.kotlin.fir.deserialization.AbstractFirDeserializedSymbolProvider.getClass$default(AbstractFirDeserializedSymbolProvider.kt:326)
at org.jetbrains.kotlin.fir.deserialization.AbstractFirDeserializedSymbolProvider.getClassLikeSymbolByClassId(AbstractFirDeserializedSymbolProvider.kt:420)
at org.jetbrains.kotlin.fir.resolve.providers.impl.FirCachingCompositeSymbolProvider.computeClass(FirCachingCompositeSymbolProvider.kt:147)
at org.jetbrains.kotlin.fir.resolve.providers.impl.FirCachingCompositeSymbolProvider.access$computeClass(FirCachingCompositeSymbolProvider.kt:27)
at org.jetbrains.kotlin.fir.resolve.providers.impl.FirCachingCompositeSymbolProvider$special$$inlined$createCache$1.invoke(FirCachesFactory.kt:163)
at org.jetbrains.kotlin.fir.resolve.providers.impl.FirCachingCompositeSymbolProvider$special$$inlined$createCache$1.invoke(FirCachesFactory.kt:147)
at org.jetbrains.kotlin.fir.caches.FirThreadUnsafeCache.getValue(FirThreadUnsafeCachesFactory.kt:57)
at org.jetbrains.kotlin.fir.resolve.providers.impl.FirCachingCompositeSymbolProvider.getClassLikeSymbolByClassId(FirCachingCompositeSymbolProvider.kt:174)
at org.jetbrains.kotlin.fir.resolve.ToSymbolUtilsKt.toSymbol(ToSymbolUtils.kt:57)
at org.jetbrains.kotlin.fir.resolve.ToSymbolUtilsKt.toRegularClassSymbol(ToSymbolUtils.kt:73)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinTypeForFlexibleBound(JavaTypeConversion.kt:240)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinTypeForFlexibleBound$default(JavaTypeConversion.kt:200)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeTypeProjection(JavaTypeConversion.kt:118)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinType(JavaTypeConversion.kt:91)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinType$default(JavaTypeConversion.kt:86)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeTypeProjection(JavaTypeConversion.kt:156)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinType(JavaTypeConversion.kt:91)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinType$default(JavaTypeConversion.kt:86)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toFirResolvedTypeRef(JavaTypeConversion.kt:79)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.resolveIfJavaType(JavaTypeConversion.kt:55)
at org.jetbrains.kotlin.fir.java.JavaTypeConversionKt.toConeKotlinTypeProbablyFlexible(JavaTypeConversion.kt:64)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.toConeKotlinType(SignatureEnhancement.kt:1024)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhance(SignatureEnhancement.kt:1013)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhance(SignatureEnhancement.kt:1006)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhanceReturnType(SignatureEnhancement.kt:902)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhanceReturnType(SignatureEnhancement.kt:851)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhance$fir_jvm(SignatureEnhancement.kt:169)
at org.jetbrains.kotlin.fir.java.enhancement.FirEnhancedSymbolsStorage$EnhancementSymbolsCache.enhancedVariables$lambda$0(SignatureEnhancement.kt:1152)
at org.jetbrains.kotlin.fir.caches.FirThreadUnsafeCache.getValue(FirThreadUnsafeCachesFactory.kt:57)
at org.jetbrains.kotlin.fir.java.enhancement.FirSignatureEnhancement.enhancedProperty(SignatureEnhancement.kt:137)
at org.jetbrains.kotlin.fir.java.scopes.JavaClassMembersEnhancementScope.processPropertiesByName$lambda$0(JavaClassMembersEnhancementScope.kt:33)
at org.jetbrains.kotlin.fir.scopes.impl.AbstractFirUseSiteMemberScope.processPropertiesByName(AbstractFirUseSiteMemberScope.kt:159)
at org.jetbrains.kotlin.fir.java.scopes.JavaClassMembersEnhancementScope.processPropertiesByName(JavaClassMembersEnhancementScope.kt:32)
at org.jetbrains.kotlin.fir.scopes.impl.FirScopeWithCallableCopyReturnTypeUpdater.processPropertiesByName(FirScopeWithCallableCopyReturnTypeUpdater.kt:40)
at org.jetbrains.kotlin.fir.scopes.FirContainingNamesAwareScopeKt.processAllProperties(FirContainingNamesAwareScope.kt:30)
at io.ktor.compiler.utils.PropertiesUtilsKt.getAllPropertiesFromType(PropertiesUtils.kt:42)
at io.ktor.openapi.model.JsonSchema$Companion.schemaDefinitionForType(JsonSchema.kt:96)
at io.ktor.openapi.model.JsonSchema$Companion.findSchemaDefinitions(JsonSchema.kt:50)
at io.ktor.openapi.routing.interpreters.CallRespondInterpreter.check$lambda$0(CallRespondInterpreter.kt:43)
at io.ktor.openapi.routing.RouteNode.resolve(RouteNode.kt:16)
at io.ktor.openapi.routing.RouteNode$CallFeature.resolve(RouteNode.kt:55)
at io.ktor.openapi.routing.RouteCollector.resolve(RouteCollector.kt:42)
at io.ktor.openapi.routing.RouteCollector.collectRoutes(RouteCollector.kt:17)
at io.ktor.openapi.OpenApiSpecGenerator.buildSpecification(OpenApiSpecGenerator.kt:17)
at io.ktor.openapi.OpenApiExtension.saveSpecification(OpenApiExtension.kt:59)
at io.ktor.compiler.KtorCompilerPluginRegistrar.registerExtensions$lambda$0(KtorCompilerPluginRegistrar.kt:27)
at org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrarKt.registerInProject$lambda$1(CompilerPluginRegistrar.kt:72)
at org.jetbrains.kotlin.com.intellij.openapi.util.ObjectTree.runWithTrace(ObjectTree.java:130)
... 37 more
Using fallback strategy: Compile without Kotlin daemon
Try ./gradlew --stop if this issue persists
If it does not look related to your configuration, please file an issue with logs to https://kotl.in/issue
exception: /a/b/c/file.kt:123:45: warning: condition is always 'true'.
exception: users.putAll(newUsers.filter { it.guid != null }.map {
exception: ^^^^^^^^^^^^^^^
// the same error but for diffrent lines
exception: ERROR: null
exception: java.lang.StackOverflowError
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.renderConstructor(ConeTypeRenderer.kt:144)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.renderConstructor(ConeTypeRendererForReadability.kt:74)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:115)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.renderBound(ConeTypeRendererForReadability.kt:49)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.render(ConeTypeRendererForReadability.kt:32)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:110)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:229)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.renderTypeArguments(ConeTypeRenderer.kt:164)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:117)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.renderBound(ConeTypeRendererForReadability.kt:49)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.render(ConeTypeRendererForReadability.kt:32)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:110)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:229)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.renderTypeArguments(ConeTypeRenderer.kt:164)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:117)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.renderBound(ConeTypeRendererForReadability.kt:49)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.render(ConeTypeRendererForReadability.kt:32)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:110)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.types.ConeTypeUtilsKt.renderReadableWithFqNames(ConeTypeUtils.kt:189)
exception: at org.jetbrains.kotlin.fir.types.ConeTypeUtilsKt.renderReadableWithFqNames$default(ConeTypeUtils.kt:187)
exception: at io.ktor.compiler.utils.FirScopedEvaluator.resolveType(ScopedExpressionEvaluator.kt:206)
exception: at io.ktor.compiler.utils.FirScopedEvaluator.resolveTypeProjection(ScopedExpressionEvaluator.kt:182)
exception: at io.ktor.openapi.routing.RouteStackKt.resolveType(RouteStack.kt:41)
exception: at io.ktor.openapi.model.JsonSchema$Companion.asJsonSchema(JsonSchema.kt:58)
exception: at io.ktor.openapi.model.JsonSchema$Companion.asJsonSchema(JsonSchema.kt:58)
// multiple lines of the same error
exception: exception in thread "main" java.lang.AssertionError
exception: at org.jetbrains.kotlin.com.intellij.openapi.diagnostic.DefaultLogger.error(DefaultLogger.java:83)
exception: at org.jetbrains.kotlin.com.intellij.openapi.diagnostic.Logger.error(Logger.java:436)
exception: at org.jetbrains.kotlin.com.intellij.openapi.util.ObjectTree.handleExceptions(ObjectTree.java:195)
exception: at org.jetbrains.kotlin.com.intellij.openapi.util.ObjectTree.runWithTrace(ObjectTree.java:141)
exception: at org.jetbrains.kotlin.com.intellij.openapi.util.ObjectTree.executeAll(ObjectTree.java:162)
exception: at org.jetbrains.kotlin.com.intellij.openapi.util.Disposer.dispose(Disposer.java:205)
exception: at org.jetbrains.kotlin.com.intellij.openapi.util.Disposer.dispose(Disposer.java:193)
exception: at org.jetbrains.kotlin.cli.common.UtilsKt.disposeRootInWriteAction$lambda$0(utils.kt:142)
exception: at org.jetbrains.kotlin.com.intellij.openapi.application.ActionsKt.runWriteAction$lambda$0(actions.kt:16)
exception: at org.jetbrains.kotlin.com.intellij.mock.MockApplication.runWriteAction(MockApplication.java:209)
exception: at org.jetbrains.kotlin.com.intellij.openapi.application.ActionsKt.runWriteAction(actions.kt:16)
exception: at org.jetbrains.kotlin.cli.common.UtilsKt.disposeRootInWriteAction(utils.kt:141)
exception: at org.jetbrains.kotlin.cli.pipeline.AbstractCliPipeline.execute(AbstractCliPipeline.kt:93)
exception: at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecutePhased(K2JVMCompiler.kt:79)
exception: at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecutePhased(K2JVMCompiler.kt:45)
exception: at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:90)
exception: at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.kt:352)
exception: at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.kt:330)
exception: at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.kt:294)
exception: at org.jetbrains.kotlin.cli.common.CLICompiler$Companion.doMainNoExit(CLICompiler.kt:431)
exception: at org.jetbrains.kotlin.cli.common.CLICompiler$Companion.doMainNoExit$default(CLICompiler.kt:426)
exception: at org.jetbrains.kotlin.cli.common.CLICompiler$Companion.doMain(CLICompiler.kt:418)
exception: at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler$Companion.main(K2JVMCompiler.kt:252)
exception: at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.main(K2JVMCompiler.kt)
exception: caused by: java.lang.StackOverflowError
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.renderConstructor(ConeTypeRenderer.kt:144)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.renderConstructor(ConeTypeRendererForReadability.kt:74)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:115)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.renderBound(ConeTypeRendererForReadability.kt:49)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.render(ConeTypeRendererForReadability.kt:32)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:110)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:229)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.renderTypeArguments(ConeTypeRenderer.kt:164)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:117)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.renderBound(ConeTypeRendererForReadability.kt:49)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.render(ConeTypeRendererForReadability.kt:32)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:110)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:229)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.renderTypeArguments(ConeTypeRenderer.kt:164)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:117)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.renderBound(ConeTypeRendererForReadability.kt:49)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRendererForReadability.render(ConeTypeRendererForReadability.kt:32)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render(ConeTypeRenderer.kt:110)
exception: at org.jetbrains.kotlin.fir.renderer.ConeTypeRenderer.render$default(ConeTypeRenderer.kt:87)
exception: at org.jetbrains.kotlin.fir.types.ConeTypeUtilsKt.renderReadableWithFqNames(ConeTypeUtils.kt:189)
exception: at org.jetbrains.kotlin.fir.types.ConeTypeUtilsKt.renderReadableWithFqNames$default(ConeTypeUtils.kt:187)
exception: at io.ktor.compiler.utils.FirScopedEvaluator.resolveType(ScopedExpressionEvaluator.kt:206)
exception: at io.ktor.compiler.utils.FirScopedEvaluator.resolveTypeProjection(ScopedExpressionEvaluator.kt:182)
exception: at io.ktor.openapi.routing.RouteStackKt.resolveType(RouteStack.kt:41)
exception: at io.ktor.openapi.model.JsonSchema$Companion.asJsonSchema(JsonSchema.kt:58)
exception: at io.ktor.openapi.model.JsonSchema$Companion.asJsonSchema(JsonSchema.kt:58)
// multiple lines of the same error
./gradlew --version
------------------------------------------------------------
Gradle 8.13
------------------------------------------------------------
Build time: 2025-02-25 09:22:14 UTC
Revision: 073314332697ba45c16c0a0ce1891fa6794179ff
Kotlin: 2.0.21
Groovy: 3.0.22
Ant: Apache Ant(TM) version 1.10.15 compiled on August 25 2024
Launcher JVM: 21.0.8 (Ubuntu 21.0.8+9-Ubuntu-0ubuntu122.04.1)
Daemon JVM: /usr/lib/jvm/java-21-openjdk-amd64 (no JDK specified, using current Java home)
OS: Linux 6.6.87.2-microsoft-standard-WSL2 amd64
buildOpenApi fails with unknown serializer
It fails here on this code
class CustomerDataToJsonbMapper(
private val json: Json = BackendComponent.json
) : Mapper<CustomerData, JSONB> {
override fun map(from: CustomerData): JSONB {
return runCatching {
val serializable = SerializableCustomerData(
email = from.email,
name = from.name,
phone = from.phone,
address = from.address?.let { addr ->
SerializableCustomerAddress(
line1 = addr.line1,
line2 = addr.line2,
city = addr.city,
state = addr.state,
postalCode = addr.postalCode,
country = addr.country
)
},
taxId = from.taxId,
metadata = from.metadata
)
val jsonString = json.encodeToString(SerializableCustomerData.serializer(), serializable)
JSONB.valueOf(jsonString)
}.getOrElse {
JSONB.valueOf("{}")
}
}
}
my Backendcomponent.json has
@OptIn(ExperimentalSerializationApi::class)
val json by lazy {
kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
useAlternativeNames = false
prettyPrint = true
prettyPrintIndent = " "
isLenient = true
explicitNulls = false
encodeDefaults = true
}
}
@Serializable
data class SerializableCustomerData(
@SerialName("email") val email: String,
@SerialName("name") val name: String? = null,
@SerialName("phone") val phone: String? = null,
@SerialName("address") val address: SerializableCustomerAddress? = null,
@SerialName("taxId") val taxId: String? = null,
@SerialName("metadata") val metadata: Map<String, String> = emptyMap()
)
@Serializable
data class SerializableCustomerAddress(
@SerialName("line1") val line1: String? = null,
@SerialName("line2") val line2: String? = null,
@SerialName("city") val city: String? = null,
@SerialName("state") val state: String? = null,
@SerialName("postalCode") val postalCode: String? = null,
@SerialName("country") val country: String? = null
)
Keep in mind that these are not even used in our routes as responses/requests but are using serialization because they're coming from webhooks
and the stacktrace is
e: file:///Users/funkymuse/WebProjects/my_project/backend/server/src/main/kotlin/com/my_project/backend/server/billing/mappers/JsonbMappers.kt:40:75 Unresolved reference 'serializer'.
e: file:///Users/funkymuse/WebProjects/my_project/backend/server/src/main/kotlin/com/my_project/backend/server/billing/mappers/JsonbMappers.kt:69:79 Unresolved reference 'serializer'.
e: file:///Users/funkymuse/WebProjects/my_project/backend/server/src/main/kotlin/com/my_project/backend/server/billing/mappers/JsonbMappers.kt:110:80 Unresolved reference 'serializer'.
Kotlin version: 2.2.21
Ktor version: 3.3.3
Task: ./gradlew buildOpenApi
Core
Ktor doesn't parse multiple headers
I think Ktor should parse headers of this type:
X-Multi-Header: Value1, Value2
into two separate headers. The method headers.getAll("X-Multi-Header") should then return listOf("Value1", "Value2").
Currently, it only returns a single element: "Value1, Value2", which seems incorrect judging by this document
e.x.
curl -v -H "X-Multi-Header: value1,value2"http://localhost:8080/curl -v -H "X-Multi-Header: value1" -H "X-Multi-Header: value2" http://localhost:8080
Spring will parse both of these requests as: listOf("value1", "value2")
but Ktor no
get("/") {
val all = call.request.headers.getAll("X-Multi-Header")
call.respondText(all!!.joinToString("\n"))
}
Support HTTP QUERY method
The proposal is not finished yet, but when it will be, Ktor support for this method will be a very nice feature.
The decodeBase64Bytes method doesn't throw an exception on illegal base64 characters
Extension function, for decoding base64 content, does not fail when provided with wrong variant of Base64.
For example:
data (hex): b7 6a 61 c0 ab 60 fa 01 93 73 f4 be ca d4 d3 17
It will be encoded as Base64 url variant: "t2phwKtg-gGTc_S-ytTTFw"
When calling: "t2phwKtg-gGTc_S-ytTTFw".decodeBase64Bytes() it will give array like (hex): b76a61c0ab60fe019373f4bfcad4d317
So the diff:
original: b7 6a 61 c0 ab 60 fa 01 93 73 f4 be ca d4 d3 17
!!
decoded: b7 6a 61 c0 ab 60 fe 01 93 73 f4 bf ca d4 d3 17
Expected result:
Either fail with exception or decode correctly.
ByteReadChannel.readUTF8Line doesn't throw TooLongLineException when the limit is reached
Gösta Steen says about Ktor documentation
Api symbol: io.ktor.utils.io.readUTF8Line:
The implemetation is wrong or the documentation is wrong.
If set max to a low value (e.g. 10) then send some char's where the first lineending is after e.g. 15 char's, the readBuffer contains more than this 10 char's and it does not throw the TooLongLineException because the check is performed after the while(!readBuffer.exhausted()). So you can receive lines longer than 10 char's without an Exception is thrown.
Best regards
Gösta Steen
Docs
Fix release log link for KTOR version 3.4.0
Current release details page has a wrong changelog link for ktor version 3.4.0 It points to 3.3.3 instead of the latest one 3.4.0
Gradle Plugin
`buildOpenApi` task is getting invoked during test tasks
Users wondered why `gradle kotest` triggers the `buildOpenApi` task. Actually, `buildOpenApi` behaves like a compile task and can run during tests. [slack discussion]
We expect that OpenAPI spec will not be generated during test runs.
IO
Redesign ByteReadChannel.readUTF8Line API
An umbrella issue for everything related to ByteReadChannel.readUTF8Line improvements
Make readUTF8LineTo return number of read symbols instead of boolean
I've encountered a problem where I need to compute offset of the last read line relative to file start. It seems to be easy to add a counter inside the function to return the number of bytes being read.
P. S. Would be nice to be able to make a feature request, not only bugs.
ByteReadChannel.readUTF8Line is inefficient for long lines
The following test takes ~2.3 seconds to complete.
@Test
fun `test reading long lines completes in reasonable time`() = runTest {
var count = 0
val numberOfLines = 100
val lineSize = 1024 * 1024 // 1MB
val line = "A".repeat(lineSize) + "\n"
val channel = writer {
repeat(numberOfLines) {
channel.writeStringUtf8(line)
}
}.channel
var actualLine: String? = channel.readUTF8Line()
while (actualLine != null) {
count++
assertEquals(lineSize, actualLine.length)
actualLine = channel.readUTF8Line()
}
assertEquals(numberOfLines, count)
}
For comparison, the test takes only ~1 second when readUTF8Line() is replaced with the following naive implementation:
suspend fun ByteReadChannel.readLineFast(): String? {
awaitContent()
return readBuffer.readLine()
}
Comparsion for different targets:
| readUTF8Line | readLineFast | Speed up | |
|---|---|---|---|
| JVM, JS | ~2.3 s | ~700 ms | x3.2 |
Native (no -opt) |
~25 s | ~3.8 s | x6.5 |
Native (with -opt) |
~1.9 s | ~700 ms | x2.7 |
Profiling on iOS shows that writeByte and readByte take ~50% of CPU time:
3.46 s 37,7 % kfun:io.ktor.utils.io.ByteReadChannel#<get-readBuffer>(){}kotlinx.io.Source-trampoline
2.60 s 28,4 % kfun:kotlinx.io.Buffer#writeByte(kotlin.Byte){}
1.97 s 21,5 % kfun:kotlinx.io.Source#readByte(){}kotlin.Byte-trampoline
515.00 ms 5,6 % kfun:kotlinx.io.Source#exhausted(){}kotlin.Boolean-trampoline
Another ~37.7% taken by get-readeBuffer which calls exhausted under the hood. Probably we should get readBuffer once before reading a line and store it into a local variable.
Infrastructure
Rename target jsAndWasmShared to web
To align it with the shared target added in Kotlin 2.2.20
Ensure we can change suffix .jsAndWasmShared.kt to .web.kt and this change is backwards compatible.
"Unresolved classifier: platform/posix/pthread_mutex_t" when executing :ktor-io:commonizeCInterop on latest main branch
Sync the project in IDEA and you'll see:
Execution failed for task ':ktor-io:commonizeCInterop'.
> Process 'command '/Library/Java/JavaVirtualMachines/zulu-21.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1
* Try:
> Run with --info or --debug option to get more log output.
> Get more help at https://help.gradle.org.
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':ktor-io:commonizeCInterop'.
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:135)
at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:288)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:133)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:121)
at org.gradle.api.internal.tasks.execution.ProblemsTaskPathTrackingTaskExecuter.execute(ProblemsTaskPathTrackingTaskExecuter.java:41)
at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:74)
at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
at org.gradle.execution.plan.DefaultNodeExecutor.executeLocalTaskNode(DefaultNodeExecutor.java:55)
at org.gradle.execution.plan.DefaultNodeExecutor.execute(DefaultNodeExecutor.java:34)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:355)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:343)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:339)
at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:84)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:339)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:328)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:459)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:376)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
Caused by: org.gradle.process.ProcessExecutionException: Process 'command '/Library/Java/JavaVirtualMachines/zulu-21.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1
at org.gradle.process.internal.DefaultExecHandle$ExecResultImpl.assertNormalExitValue(DefaultExecHandle.java:450)
at org.gradle.process.internal.DefaultJavaExecAction.execute(DefaultJavaExecAction.java:58)
at org.gradle.process.internal.DefaultExecActionFactory.javaexec(DefaultExecActionFactory.java:209)
at org.gradle.process.internal.DefaultExecOperations.javaexec(DefaultExecOperations.java:42)
at org.jetbrains.kotlin.internal.compilerRunner.native.KotlinNativeToolRunner.runViaExec(KotlinNativeToolRunner.kt:117)
at org.jetbrains.kotlin.internal.compilerRunner.native.KotlinNativeToolRunner.runTool(KotlinNativeToolRunner.kt:70)
at org.jetbrains.kotlin.compilerRunner.GradleCliCommonizerKt$GradleCliCommonizer$1.invoke(GradleCliCommonizer.kt:39)
at org.jetbrains.kotlin.commonizer.CliCommonizer.commonizeLibraries(CliCommonizer.kt:49)
at org.jetbrains.kotlin.gradle.targets.native.internal.CInteropCommonizerTask.commonize(CInteropCommonizerTask.kt:244)
at org.jetbrains.kotlin.gradle.targets.native.internal.CInteropCommonizerTask.access$commonize(CInteropCommonizerTask.kt:50)
at org.jetbrains.kotlin.gradle.targets.native.internal.CInteropCommonizerTask$commonizeCInteropLibraries$1.invoke(CInteropCommonizerTask.kt:231)
at org.jetbrains.kotlin.gradle.targets.native.internal.CInteropCommonizerTask$commonizeCInteropLibraries$1.invoke(CInteropCommonizerTask.kt:230)
at org.jetbrains.kotlin.compilerRunner.ReportUtilsKt.addBuildMetricsForTaskAction(reportUtils.kt:260)
at org.jetbrains.kotlin.gradle.targets.native.internal.CInteropCommonizerTask.commonizeCInteropLibraries(CInteropCommonizerTask.kt:230)
at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:125)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute(StandardTaskAction.java:58)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:51)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:29)
at org.gradle.api.internal.tasks.execution.TaskExecution$3.run(TaskExecution.java:252)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeAction(TaskExecution.java:237)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeActions(TaskExecution.java:220)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeWithPreviousOutputFiles(TaskExecution.java:203)
at org.gradle.api.internal.tasks.execution.TaskExecution.execute(TaskExecution.java:170)
at org.gradle.internal.execution.steps.ExecuteStep.executeInternal(ExecuteStep.java:105)
at org.gradle.internal.execution.steps.ExecuteStep.access$000(ExecuteStep.java:44)
at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:59)
at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:56)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:56)
at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:44)
at org.gradle.internal.execution.steps.CancelExecutionStep.execute(CancelExecutionStep.java:42)
at org.gradle.internal.execution.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:75)
at org.gradle.internal.execution.steps.TimeoutStep.execute(TimeoutStep.java:55)
at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:50)
at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:28)
at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:68)
at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:38)
at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:61)
at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:26)
at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:69)
at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:46)
at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:39)
at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:28)
at org.gradle.internal.execution.steps.BuildCacheStep.executeWithoutCache(BuildCacheStep.java:189)
at org.gradle.internal.execution.steps.BuildCacheStep.executeAndStoreInCache(BuildCacheStep.java:145)
at org.gradle.internal.execution.steps.BuildCacheStep.lambda$executeWithCache$4(BuildCacheStep.java:101)
at org.gradle.internal.execution.steps.BuildCacheStep.lambda$executeWithCache$5(BuildCacheStep.java:101)
at org.gradle.internal.Try$Success.map(Try.java:170)
at org.gradle.internal.execution.steps.BuildCacheStep.executeWithCache(BuildCacheStep.java:85)
at org.gradle.internal.execution.steps.BuildCacheStep.lambda$execute$0(BuildCacheStep.java:74)
at org.gradle.internal.Either$Left.fold(Either.java:116)
at org.gradle.internal.execution.caching.CachingState.fold(CachingState.java:62)
at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:73)
at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:48)
at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:46)
at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:35)
at org.gradle.internal.execution.steps.SkipUpToDateStep.executeBecause(SkipUpToDateStep.java:75)
at org.gradle.internal.execution.steps.SkipUpToDateStep.lambda$execute$2(SkipUpToDateStep.java:53)
at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:53)
at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:35)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:37)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:27)
at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:49)
at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:27)
at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:71)
at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:39)
at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:64)
at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:35)
at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:62)
at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:40)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:76)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:45)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.executeWithNonEmptySources(AbstractSkipEmptyWorkStep.java:136)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:61)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:38)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:38)
at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:36)
at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:23)
at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:75)
at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:41)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.lambda$execute$0(AssignMutableWorkspaceStep.java:35)
at org.gradle.api.internal.tasks.execution.TaskExecution$4.withWorkspace(TaskExecution.java:297)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:31)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:22)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:40)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:23)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.lambda$execute$2(ExecuteWorkBuildOperationFiringStep.java:67)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:67)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:39)
at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:46)
at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:34)
at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:44)
at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:31)
at org.gradle.internal.execution.impl.DefaultExecutionEngine$1.execute(DefaultExecutionEngine.java:64)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:132)
... 30 more
which is due to https://github.com/ktorio/ktor/blob/5072b016d1b02cc1224f077a508e351f5fd2e4fb/ktor-io/build.gradle.kts#L13
After removing all createCInterop invocations or emptying createCInterop (like this), the sync process goes to continuation.
My env:
IntelliJ IDEA 2025.3
Build #IU-253.28294.334, built on December 5, 2025
Source revision: f50d587f27abb
Licensed to Shadow Gradle plugin / Zongle Wang
Subscription is active until August 19, 2026.
For non-commercial open source development only.
Runtime version: 21.0.8+9-b1163.69 aarch64 (JCEF 137.0.17)
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
Toolkit: sun.lwawt.macosx.LWCToolkit
macOS 26.1
GC: G1 Young Generation, G1 Concurrent GC, G1 Old Generation
Memory: 10240M
Cores: 11
Metal Rendering is ON
Registry:
ide.experimental.ui=true
kotlin.scripting.index.dependencies.sources=true
jetbrains.security.package-checker.enableNpmTransitiveDependencies=false
Non-Bundled Plugins:
com.intellij.debugger.collections.visualizer (253.28294.336)
org.editorconfig.editorconfigjetbrains (253.28294.335)
com.jetbrains.kmm (0.9-253.28294-IJ-334)
intellij.webp (253.28294.218)
com.intellij.nativeDebug (253.28294.251)
org.jetbrains.amper (253.28294.325)
org.jetbrains.android (253.28294.334)
com.android.tools.design (253.28294.251)
com.gradle.develocity.ide (1.1.0)
androidx.compose.plugins.idea (253.28294.251)
com.github.copilot (1.5.62-243)
org.jetbrains.security.package-checker (253.28294.335)
Kotlin: 253.28294.334-IJ
Network
Excessive memory allocations while writing bytes into write channel of TCP/IP socket
I am using the aSocket/network API for TCP connection on Android.
I made a simple code to send a byte every second like that:
private suspend fun tcpClientWrite(
remoteAddress: SocketAddress
) = withContext(Dispatchers.IO) {
val clientSocket = aSocket(selectorManager).tcp().connect(remoteAddress)
val connection = clientSocket.connection()
while (isActive) {
Log.i("TcpClient", "Sending a new byte: ${Thread.currentThread().id}")
connection.output.writeByte(0x01) // Example of writing a byte
connection.output.flush()
delay(1000)
}
clientSocket.close()
}
I expect this code to have a rather low memory impact.
Unfortunately, with the Android Studio profiling tool, I notice that the writeBytes keeps creating 8 208 byte segment although there is a segment pool.
{width=70%}
Is there a way to avoid the creation of this segments for a single byte?
Server
Netty: RejectedExecutionException during shutdown on MacOS when dev mode is enabled
Steps to reproduce:
- Create a new Ktor project from https://start.ktor.io/settings with Netty as engine and Maven as build tool. Java 21 is used
- Add content negotiation and kotlinx-serialization plugins
- Download, extract and open the project in Intellij
- Run the project from the IDE
- Open a browser and hit http;//localhost:8080/ multiple times
- Stop using Ctrl + F2
What is expected: A clean shutdown
What is observed: eption as below
java.util.concurrent.RejectedExecutionException: event executor terminated
at io.netty.util.concurrent.SingleThreadEventExecutor.reject(SingleThreadEventExecutor.java:1005)
at io.netty.util.concurrent.SingleThreadEventExecutor.offerTask(SingleThreadEventExecutor.java:388)
at io.netty.util.concurrent.SingleThreadEventExecutor.addTask(SingleThreadEventExecutor.java:381)
at io.netty.util.concurrent.SingleThreadEventExecutor.execute(SingleThreadEventExecutor.java:907)
at io.netty.util.concurrent.SingleThreadEventExecutor.execute0(SingleThreadEventExecutor.java:873)
at io.netty.util.concurrent.SingleThreadEventExecutor.execute(SingleThreadEventExecutor.java:863)
at io.netty.channel.DefaultChannelPipeline.destroyUp(DefaultChannelPipeline.java:816)
at io.netty.channel.DefaultChannelPipeline.destroy(DefaultChannelPipeline.java:801)
at io.netty.channel.DefaultChannelPipeline.access$700(DefaultChannelPipeline.java:45)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelUnregistered(DefaultChannelPipeline.java:1411)
at io.netty.channel.DefaultChannelPipeline.fireChannelUnregistered(DefaultChannelPipeline.java:780)
at io.netty.channel.AbstractChannel$AbstractUnsafe$8.run(AbstractChannel.java:692)
at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:148)
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:141)
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:507)
at io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:182)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1073)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:1583)
The above issue is not observed when using Ktor v3.1.3
Support OpenAPI specification for the Ktor Client and Server Application
Currently, Ktor lacks a built-in way to generate OpenAPI specifications from routes. This forces developers to either:
- Manually maintain separate OpenAPI specification files
- Rely on third-party libraries with limited maintenance and compatibility
Ktor Library Improvement Proposal (KLIP)
https://github.com/ktorio/ktor-klip/blob/openapi/proposals/0004-open-api.md
Read OpenAPI security details from authentication plugin
In order to provide the security scheme for the OpenAPI specification at runtime, we'll need to infer the relevant information from our Authentication object model stored in the Application attributes.
This can probably be an extension function so that we can call something like:
routing {
get("spec.json") {
call.respond(
openApiSpecification {
info = call.application.openApiTopLevelInfo()
pathItems = call.application.routingRoot.findPathItems()
components {
securitySchemes = call.application.findSecuritySchemes()
}
}
)
}
}
This ought to populate our security schemes from the information provided in the authentication plugin.
DI: Allow file configuration
When using DI with modules configured from file, it's easy to hit the duplicate plugin install exception. We should provide some file configuration options for DI.
How to detect if a request was cancelled from client on Ktor server
I use ktor on server and have a request that may be executed for a long time (one minute or more). I want to detect client request cancellation to release resources and cancel the long running operation.
Take the following simple example:
routing {
get("/very_long_operation") {
for(e in 1..100){
delay(1000)
if( isCancelledByClient || isSocketConnectionBroken ) {
println("stop/cancel coroutine & release resources")
coroutineContext.cancel()
}
println("operation still running: $e")
}
call.respondText("Done")
}
}
Where isCancelledByClient and isSocketConnectionBroken are variable wanted so that we know if we need to interrupt the current operation and release resource.
Is there any way to do it?
Support SIGINT on web and SIGTERM on Native
JS is only registered to gracefully shut down on SIGTERM, and Native is only registered to gracefully shutdown on SIGINT.
While JVM gracefully shut down on both SIGTERM, and SIGINT.
Routing documentation runtime API
This ticket is to introduce the API for embedding OpenAPI information into our routing tree, so that it's available at runtime.
Zstd support import changes fixes
Introduce/reuse interfaces for logging selectors
Comment from @osip.fatkullin about KTOR-7639:
- Maybe we should introduce a supertype or interface for selectors that should be included in path. That would make it possible to correctly render custom path selectors.
- Maybe we can reuse for this interfaces introduced in KTOR-8936 Routing documentation API #5125 (fyi @bjhham)
CORS: Excessive logs on INFO level since 3.3.3
After upgrading to 3.3.3, we're getting two additional log lines with every request from the CORS handler. This makes a lot of noise for engineers to sift through, and additionally is costly when logs are ingested through services like Datadog.
The amount of information that gets logged at the default info level is excessive.
Annotate `Route`s security based on the `Authentication` plugin.
install(Authentication) {
oauth("oauth") {
urlProvider = { "http://localhost/callback" }
settings = OAuthServerSettings.OAuth2ServerSettings(
// omit some fields in this example
defaultScopes = listOf("profile", "email")
)
client = HttpClient()
}
}
routing {
val authenticatedRoute = authenticate("oauth") {
get("/test") {
call.respond("authenticated")
}
}
// the following annotation should be added automatically
// when generating OpenAPI spec
authenticatedRoute.annotate {
security {
requirement("", listOf("profile", "email"))
}
}
}
JettyKtorHandler executor will never grow beyond core size
JettyKtorHandler has the following code
private val queue: BlockingQueue<Runnable> = LinkedBlockingQueue()
private val executor = ThreadPoolExecutor(
configuration.callGroupSize,
configuration.callGroupSize * 8,
THREAD_KEEP_ALIVE_TIME,
TimeUnit.MINUTES,
queue
) { r ->
Thread(r, "ktor-jetty-$environmentName-${JettyKtorCounter.incrementAndGet()}")
}
I believe the intention is to have an executor of size configuration.callGroupSize, burstable to configuration.callGroupSize * 8 and then queue the tasks once that has been reached. However, this is not the case since ThreadPoolExecutor will only increase number of threads above core size if queue.offer() returns false which it never will since it is unbounded. See https://stackoverflow.com/a/19528305/422924 . However I am not a fan of the "solution" described.
I will instead suggest that queue is replaced with SynchronousQueue<Runnable>() which is what Executors uses in the non-fixed thread count cases. The new resulting behavior is the same except that, after growing to configuration.callGroupSize * 8 threads (which is quite a fair amount) then instead of queuing tasks it will block and provide much needed back pressure to Jetty.
I will provide a PR soon.
Support for `respondResource`
We currently support responding with a single static file using respondFile; however, we lack an equivalent function for responding with resource references.
See slack thread:
https://jetbrains.slack.com/archives/C07U498LLUR/p1759133011227279
Auth API key plugin
Hello!
There are currently ktor server auth jwt and ktor server auth ldap plugins, but no ktor server auth api key or similar.
This is something really common to have api keys in the backend. So I think we should add this feature in the default available official plugins.
I'm willing to contribute and do the whole module if you want me to. I just would like to ensure the idea gets accepted first, as well as the exact module name is chosen. If so, let me know and I'll start working on it! (with configuration options like header name, key validator, ...) based on existing server auth architecture.
Partial HTML response
Using the kotlinx-html DSL, I would like to be able to respond to requests without wrapping everything in a top-level <html> element.
Read OpenAPI default content type information from ContentNegotiation plugin
We should be able to populate routes at runtime based on information provided from the ContentNegotiation plugin.
We'll need to introduce some placeholder content type that will be substituted out when building the Operation objects from the plugins. So when encountering something like */* in the models, it can be replaced with the default type specified in the content negotiation plugin. This can vary based on the Accept header, so the substitution may not always be 1:1.
EngineMain: Support reading trust store settings from the configuration
It's not possible to specify Ktor Server Trust Store when using EngineMain with Configuration in YAML/HOCON file.
With embeddedServer it can be done this way:
embeddedServer(Netty, configure = {
sslConnector(
keyStore,
keyAlias,
{ keyStorePassword.toCharArray() },
{ privateKeyPassword.toCharArray() }
) {
// These parameters can only be specified via embeddedServer
this.trustStore = trustStore
this.trustStorePath = trustStorePath
this.enabledProtocols = enabledProtocols
}
}).start(wait = true)
There are the following SSL properties:
- ktor.security.ssl.keyStore
- ktor.security.ssl.keyAlias
- ktor.security.ssl.keyStorePassword
- ktor.security.ssl.privateKeyPassword
The list should be extended with these properties:
- ktor.security.ssl.trustStore
- ktor.security.ssl.trustStorePath
- ktor.security.ssl.enabledProtocols
Ktor Oauth2 feature sends 401 response when the client secret is invalid
At our organization, we started to develop an application for JetBrains Space, which uses the Authorization-Authentication-Flow.
The Oauth2 Feature for Ktor works really well in this use-case.
There is only one problem:
When the clientSecret is changed/regenerated and a user wants to authorize with JetBrains space using an outdated clientSecret we get a 401 error.
The thing is that this error response is directly sent to the client, so our users are stuck with an unusable page and no way to recover from this situation. There is still an active session token remaining for the user, which would need to be deleted (which cant be done unless one knows how to use the devtools).
As far as i can tell there is no "Hook" within the oauth2 module, which would allow me to take over when the authentication fails and send a redirect to a login page.
This is the way our route requiring authentication looks:
authenticate(AUTH_TYPE_OAUTH2) {
route("/{service}") {
get {
val principal = call.authentication.principal<OAuthAccessTokenResponse>() as? OAuthAccessTokenResponse.OAuth2
if (principal != null) {
call.respondText("Yay, you did it", status = HttpStatusCode.OK)
} else {
call.respondText("Failed to login", status = HttpStatusCode.Unauthorized)
}
}
}
}
The principal is always non-null, so I can't find out this way if the request for authentication has failed.
If I use the optional argument for the authenticate-Method the principal is always null.
If the authentication fails due to an invalid secret the route isn't even being handled, instead, the interceptor just closes the request early and sends a 401 directly to the client. I can only then see the following log message:
2021-03-30 14:37:46.740 [nioEventLoopGroup-4-6 @call-handler#4230] TRACE io.ktor.auth.Authentication - Responding unauthorized because of error Failed to request OAuth2 access token due to io.ktor.client.features.ClientRequestException: Client request(https://<MY SECRET ORGANIZATION>.jetbrains.space:443/oauth/token) invalid: 401 Unauthorized
I am new to Ktor, so this could be entirely up to me misunderstanding the framework. If that is the case i apologize.
SSE: The handler adds `Connection: Keep-Alive` header, which is incompatible with HTTP/2
The header is unconditionally added at https://github.com/ktorio/ktor/blob/94875ac85033fffd4d21ff5b34ba52c812b84922/ktor-server/ktor-server-plugins/ktor-server-sse/common/src/io/ktor/server/sse/Routing.kt#L181 which will be rejected by netty if HTTP2 is enabled, leading to io.netty.handler.codec.http2.Http2Exception$StreamException: Illegal connection-specific header 'connection' encountered. - I have reproduced this issue using the sample project again GET https://localhost:8443/hello.
This is the route for reference
fun Application.configureRouting() {
install(StatusPages) {
exception<Throwable> { call, cause ->
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
}
}
install(Resources)
install(SSE)
install(AutoHeadResponse)
routing {
staticResources("/static", "static")
get<Articles> { article ->
// Get all articles ...
call.respond("List of articles sorted starting from ${article.sort}")
}
sse("/hello") {
send(ServerSentEvent("world"))
}
staticResources("/", "/web")
}
}
This is the entire trace for reference:
nghttp -vn https://localhost:8443/hello
[ 0.021] Connected
[WARNING] Certificate verification failed: unable to verify the first certificate
The negotiated protocol: h2
[ 0.027] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
(niv=2)
[SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[ 0.028] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
(dep_stream_id=0, weight=201, exclusive=0)
[ 0.028] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
(dep_stream_id=0, weight=101, exclusive=0)
[ 0.028] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
(dep_stream_id=0, weight=1, exclusive=0)
[ 0.028] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
(dep_stream_id=7, weight=1, exclusive=0)
[ 0.028] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
(dep_stream_id=3, weight=1, exclusive=0)
[ 0.028] send HEADERS frame <length=44, flags=0x25, stream_id=13>
; END_STREAM | END_HEADERS | PRIORITY
(padlen=0, dep_stream_id=11, weight=16, exclusive=0)
; Open new stream
:method: GET
:path: /hello
:scheme: https
:authority: localhost:8443
accept: */*
accept-encoding: gzip, deflate
user-agent: nghttp2/1.59.0
[ 0.032] recv SETTINGS frame <length=6, flags=0x00, stream_id=0>
(niv=1)
[SETTINGS_MAX_HEADER_LIST_SIZE(0x06):8192]
[ 0.032] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
; ACK
(niv=0)
[ 0.032] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
; ACK
(niv=0)
[ 0.034] recv (stream_id=13) :status: 200
[ 0.034] recv (stream_id=13) content-type: text/event-stream
[ 0.034] recv (stream_id=13) cache-control: no-store
[ 0.034] [ERROR] Invalid HTTP header field was received: frame type: 1, stream: 13, name: [connection], value: [keep-alive]
[ 0.034] [INVALID; error=Invalid HTTP header field was received] recv HEADERS frame <length=134, flags=0x04, stream_id=13>
; END_HEADERS
(padlen=0)
; First response header
[ 0.034] send RST_STREAM frame <length=4, flags=0x00, stream_id=13>
(error_code=PROTOCOL_ERROR(0x01))
[ 0.034] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
(last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])
Some requests were not processed. total=1, processed=0
Netty HTTP2 server hangs on the plugin exception
Netty HTTP2 (with SSL enabled) server hangs on the plugin exception, until the client hits a timeout. It looks like the issue occurs only with SSL, and if an exception is thrown in `onCallRespond`.
The reproducer project: https://github.com/zibet27/ktor-netty-server-hang-reproducer
Clients used for tests: Apache5, Jetty, Java, and OkHttp. All encounter the same issue.
Incorrect KDoc of ApplicationConfig.tryGetStringList
Ryuhei Furuta says about Ktor documentation
Api symbol: io.ktor.server.config.tryGetStringList:
Docs of this function seemed incorrect.
I guess it should be> Try read list of String values from ApplicationConfig.
instead of
> Try read String value from ApplicationConfig.
Remove kotlinx-datetime from ktor-server-default-headers dependencies
It should never have been exposed through transitive dependencies was it is an internal implementation detail.
DI: JobCancellationException during cleanup
- Create a new project with DI plugin.
- Start the server in Intellij using run configuration (not gradle)
- Stop the application.
Expected: shuts down gracefully
Actual:
2025-08-21 10:04:03.728 [KtorShutdownHook] WARN Application - Exception during cleanup for com.example.GreetingService?; continuing
kotlinx.coroutines.JobCancellationException: Job was cancelled
This seems to happen with all engines.
Add override DI conflict policy
We should have some Override ootb conflict policy for people who prefer to override existing instances provided.
Shared
Support Jackson 3
I want Jackson 3 support in Ktor.
I'm using Spring Boot 4.0. The default Jackson version is now 3.0.
But Ktor does not support Jackson 3, so I can't migrate some Jackson databind annotations on some beans.
Expose plusIsSpace in parseUrlEncodedParameters
Expose plusIsSpace as an option. Without it, there's no way to decode "+" chars.
HTMX: Missing DSL for some attributes
Aware it is only alpha right now, so feel free to dismiss. But did a transition for a demo project to the HTMX integration, and you might be interested in seeing what was missing.
Get that not everything will be supported, but put and indicator seems like something it should at least. :-)
Comments indicating I didn't find support: https://github.com/search?q=repo%3Aanderssv%2Fkotlin-htmx+"NOT-IN-DSL%3A"&type=code
Great work!
Anders,
Test Infrastructure
The ktor-server-test-host module, having `junit-jupiter` runtime dependency, causes conflicts
I'm trying to update Ktor from 3.1.3 to 3.3.1. No other libraries are being changed in build.gradle.kts.
I use Objectify v6.1.3. For my tests, I have two extensions: one creates a Datastore and the other creates an ObjectifyService instance that it later closes.
With ktor 3.1.3, it works fine. With ktor 3.3.1, those objects are being closed a second time, causing failures in the tests. I know it's an extra close because I can prevent at least the ObjectifyService closure by removing it from the context storage in afterEach like this:
storage.remove(ObjectifyService::class.java)
This happens even with tests that don't access any Ktor classes or methods! (Presumably because some test in the suite does do so.)
Here's how my test looks at the top:
@ExtendWith(
LocalDatastoreExtension::class,
ObjectifyExtension::class,
)
class SomeTest {
...
}
I get these errors:
Nov 08, 2025 10:54:21 A.M. com.google.cloud.datastore.DatastoreImpl close
WARNING: Failed to close channels
java.lang.UnsupportedOperationException: Not implemented.
at com.google.cloud.datastore.spi.v1.HttpDatastoreRpc.close(HttpDatastoreRpc.java:219)
at com.google.cloud.datastore.DatastoreImpl.close(DatastoreImpl.java:189)
at org.junit.jupiter.engine.descriptor.AbstractExtensionContext.lambda$createCloseAction$2(AbstractExtensionContext.java:107)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore$EvaluatedValue.close(NamespacedHierarchicalStore.java:397)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore$EvaluatedValue.access$800(NamespacedHierarchicalStore.java:381)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore.lambda$close$3(NamespacedHierarchicalStore.java:135)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore.lambda$close$4(NamespacedHierarchicalStore.java:135)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:395)
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:261)
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:261)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore.close(NamespacedHierarchicalStore.java:135)
at org.junit.jupiter.engine.descriptor.AbstractExtensionContext.close(AbstractExtensionContext.java:125)
at org.junit.jupiter.engine.execution.JupiterEngineExecutionContext.close(JupiterEngineExecutionContext.java:50)
at org.junit.jupiter.engine.descriptor.JupiterTestDescriptor.cleanUp(JupiterTestDescriptor.java:215)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$cleanUp$1(TestMethodTestDescriptor.java:176)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.cleanUp(TestMethodTestDescriptor.java:176)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.cleanUp(TestMethodTestDescriptor.java:70)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$cleanUp$10(NodeTestTask.java:173)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.cleanUp(NodeTestTask.java:173)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:104)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:161)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:147)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:145)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:144)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:101)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:161)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:147)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:145)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:144)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:101)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.executeEngine(EngineExecutionOrchestrator.java:230)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.failOrExecuteEngine(EngineExecutionOrchestrator.java:204)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:172)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:101)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:64)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:150)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:63)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:109)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:91)
at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
at org.junit.platform.launcher.core.InterceptingLauncher.lambda$execute$1(InterceptingLauncher.java:39)
at org.junit.platform.launcher.core.ClasspathAlignmentCheckingLauncherInterceptor.intercept(ClasspathAlignmentCheckingLauncherInterceptor.java:25)
at org.junit.platform.launcher.core.InterceptingLauncher.execute(InterceptingLauncher.java:38)
at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:124)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:99)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:94)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:63)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:92)
at jdk.proxy1/jdk.proxy1.$Proxy4.stop(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:200)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:132)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:121)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Failed to close extension context
org.junit.platform.commons.JUnitException: Failed to close extension context
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.IllegalStateException: You have already destroyed the Objectify context.
at com.googlecode.objectify.ObjectifyFactory.close(ObjectifyFactory.java:419)
at com.googlecode.objectify.impl.ObjectifyImpl.close(ObjectifyImpl.java:264)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:395)
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:261)
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:261)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
... 2 more
You have already destroyed the Objectify context.
java.lang.IllegalStateException: You have already destroyed the Objectify context.
at com.googlecode.objectify.ObjectifyFactory.close(ObjectifyFactory.java:419)
at com.googlecode.objectify.impl.ObjectifyImpl.close(ObjectifyImpl.java:264)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:395)
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:261)
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:261)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
testApplication: Test HTTP client does not use specified coroutine dispatcher
The Ktor HTTP client does not seem to inherit the specified coroutine dispatcher from testApplication. I would expect this to skip the timeout delay and return HTTP 504 immediately. Instead, this test blocks for 20 seconds. It seems like DelegatingTestClientEngine defaults the dispatcher to the IO dispatcher and there is no way to override this in the HTTP client builder either.
package io.ktor.server.testing.client
import io.ktor.client.request.get
import io.ktor.server.routing.get
import io.ktor.server.testing.testApplication
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.withTimeout
import kotlin.test.Test
import kotlin.time.Duration.Companion.seconds
class ApplicationTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `test delay`() = testApplication(UnconfinedTestDispatcher()) {
routing {
get("/") {
withTimeout(20.seconds) {
suspendCancellableCoroutine { }
}
}
}
val start = System.currentTimeMillis()
client.get("/")
val end = System.currentTimeMillis()
println(end - start)
}
}
Other
Use runtime-generated spec for OpenAPI / Swagger plugins
We currently have two plugins for serving OpenAPI specs in a user-friendly UI:
We'll need to support generating the model from the information attached to the routes at runtime. For the full support of OpenAPI, we'll include automatic generation of this documentation from a compiler plugin, but this is optional.
You can find an example of how to traverse the route details for populating pathItems from RouteAnnotationApiTest.
Other information will need to be supplied from:
- The
ContentNegotiationplugin - The
Authenticationplugins.
Openapi Tag Representation
Right now, the user can use the @OpenAPITag annotation to define a tag in the openApi.
/**
* @OpenAPITag TagName
**/
post("/receive") {
call.respond(HttpStatusCode.OK)
}
I don't think this is the ktor way.
I suggest creating a new extension functions in the openApi plugin.
fun RoutingBuilder.tags(vararg name: String) {}
This is gonna help us to add tags to our openApi generator in Idea and looks more understandable for users. We can add such methods for operationId, etc. This is gonna help users to have a clear specification, and also create a clearer analysis.
Or I can create highlighting and completion for this (* @OpenAPITag TagName)
Respond with an exception message
When you throw an exception in the route, the error message isn't returned in the response, only the status code.
Routing documentation compiler plugin
This is the next step for the full OpenAPI generation story.
We'll need to modify the compiler plugin so that it introduces calls for the runtime API.
Generate JSON schema for type references when using Jackson and Gson
We currently generate JSON schema from type references only using the kotlinx-serialization framework. We'll need to also generate the schema from Jackson and Gson when in the JVM and using these libraries for content negotiation.
The best way to handle this will be generating a SerialiDescriptor (kotlinx-serialization) by traversing the type using reflection, then passing the descriptor to the JsonSchema generator. This will eventually be supported by kotlinx-serialization directly, but we can implement the prototype here.
Zstd support
Are there any plans for supporting zstd compression? I saw an old issue about Brotli-Support and the first answer was that Brotli didn't have JNI Encoder. As soon as I understand that https://github.com/luben/zstd-jni have both for zstd, compression and decompression (as soon as I know its same as encode and decode)
Deprecate Apache 4 engine
As it turns out, the Apache 4.* client is end-of-life now, so we should look into deprecating this feature since it's no longer supported.
I discovered this after logging a bug for an infinite loop in the TLS handshake https://issues.apache.org/jira/browse/HTTPCORE-764
Test iOS target of the WebRTC Client in Ktor-Chat
Manually test thektor-client-webrtc on iOS by adding an iOS target for the KtorChat example application.
`audio/x-matroska` is wrongly recognized as `mkv` type
audio/x-matroska should be mka instead of mkv, it's wrongly placed at
you can see the correct sample at
Add duplex streaming for OkHttpClient
Subsystem
OkHttpClient
Motivation
With regular HTTP calls the request always completes sending before the response may begin receiving. With duplex the request and response may be interleaved. That is, request body bytes may be sent after response headers or body bytes have been received.
Solution
The feature consists of 3 main parts:
- set
isDuplex = trueforokhttp3.RequestBodyto force OkHttp to use duplex streaming - launch
writeToin the call context instead of joining it to return output stream before consuming whole input stream, I was inspired by smithy implementation (https://github.com/smithy-lang/smithy-kotlin/blob/main/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/StreamingRequestBody.kt#L63) - use
copyToinstead ofwriteAllto propagate channel flushes
More details and PR: https://github.com/ktorio/ktor/pull/5168
Missing function ByteReadChannel.readTo(sink: RawSink, byteCount: Long)
Copy of the GitHub issue.
The following function would be nice to have:
ByteReadChannel.readTo(sink: RawSink, byteCount: Long) [io.ktor.utils.io]
It would allow to store directly into a file (instead of copying data in between.
...
Upgrade to Kotlin 2.3
- [x] Bump language level to 2.3
- [x] Update Kotlin version badge in readme
- [ ] KTOR-9256 Update the compiler plugin
- [ ] (Nice to have) Enable
-Xreturn-value-checker=check
Documentation for Upgrade to Kotlin 2.3
Description
We've updated Kotlin to 2.3.0. API level is still 2.2 so Ktor is compatible with Kotlin 2.2+
Code example
N/A
Migration guide
Does this change require a migration guide for existing users?
- [ ] Yes
- [x] No
Documentation for How to detect if a request was cancelled from client on Ktor server
Please provide the following details so the technical writing team can begin work on this issue.
Description
Describe what this feature or change does and what problem it solves.
Code example
Provide a code example or link to a working sample (if applicable).
You can also link to a repo, PR, or snippet.
Migration guide
Does this change require a migration guide for existing users?
- [ ] Yes
- [ ] No
Related Links (Optional)
Add any relevant references:
- Blog posts
- Design or product docs (e.g., Quip, KEEP)
- PRs or related issues (other than the parent issue)
Once the description is complete, please re-assign this issue to a Technical Writer.
Documentation for OpenAPI compiler plugin
Description
The OpenAPI compiler plugin is included in the Ktor gradle plugin and used for generating OpenAPI documentation from both KDoc-like comments and Ktor API calls within your route handlers.
Code example
See sample project https://github.com/ktorio/ktor-build-plugins/blob/7a5f463ebccb1a7aa1a422d070022e76cafa3022/samples/ktor-openapi-sample/build.gradle.kts
// build.gradle.kts
ktor {
openApi {
// global control for the compiler plugin
enabled = true
// enable / disable inferring details from call handler code
codeInferenceEnabled = true
// toggle whether or not analysis should be applied to all routes or only those which are commented
onlyCommented = false
}
}
Migration guide
Does this change require a migration guide for existing users?
- [x] Yes
- [ ] No
For people using the experimental preview from 3.3.0, the properties for the OpenAPI extension have changed.
Previously, the plugin would generate a full OpenAPI document, but now it generates code to provide information at runtime.
The experimental preview properties looked like:
// build.gradle.kts
ktor {
@OptIn(OpenApiPreview::class)
openApi {
target = project.layout.projectDirectory.file("api.json")
title = "OpenAPI example"
version = "2.1"
summary = "This is a sample API"
}
}
And now they look like this:
// build.gradle.kts
ktor {
openApi {
// global control for the compiler plugin
enabled = true
// enable / disable inferring details from call handler code
codeInferenceEnabled = true
// toggle whether or not analysis should be applied to all routes or only those which are commented
onlyCommented = false
}
}
Properties like title and version have been deprecated and will be ignored. We now only have flags for toggling features in the compiler plugin.
We've also removed the buildOpenApi gradle task, and the compiler plugin will be included on normal compilation for the project. Now, when changes are made, they should always be reflected in the server without any extra steps.
Documentation for Routing documentation runtime API
Description
This introduces a new module for generating API documentation by introducing OpenAPI metadata to your API routes.
This API can be used as a standalone extension or in conjunction with Ktor's OpenAPI compiler plugin for the automatic generation of these calls. There will be some scenarios where automatic generation could be insufficient, and this API should fill in any gaps.
Code example
The module introduces several functions for embedding and retrieving API information into your running application.
To embed operation details into a Ktor route, you can call the annotate extension function on any route:
get("/messages") {
val query = call.parameters["q"]?.let(::parseQuery)
call.respond(messageRepository.getMessages(query))
}.annotate {
parameters {
query("q") {
description = "An encoded query"
required = false
}
}
responses {
HttpStatusCode.OK {
description = "A list of messages"
schema = jsonSchema<List<Message>>()
extension("x-sample-message", testMessage)
}
HttpStatusCode.BadRequest {
description = "Invalid query"
ContentType.Text.Plain()
}
}
summary = "get messages"
description = "Retrieves a list of messages."
}
For some usage examples, see RouteAnnotationApiTest.kt
For the builder DSL, see Operation.kt. It's quite large, so I'll try to provide a supplemental breakdown document.
More information for building the spec for different uses will be provided in:
KTOR-8993 Use runtime-generated spec for OpenAPI / Swagger plugins
Migration guide
Does this change require a migration guide for existing users?
- [ ] Yes
- [x] No
Documentation for Support shared web source set
Description
Kotlin 2.2.20 introduced a new shared source set for JS and Wasm/JS targets. After this change users can add Ktor as a dependency to the web source sets and share common web-specific parts between JS and Wasm/JS.
Code example
// build.gradle.kts
kotlin {
sourceSets {
webMain.dependencies {
implementation("io.ktor:ktor-client-js:3.4.0")
}
}
}
// src/webMain/kotlin/Main.kt
// Here we can use entities available both for JS and Wasm/JS.
// For example, the Js client engine.
actual fun createClient(): HttpClient = HttpClient(Js)
Migration guide
Does this change require a migration guide for existing users?
- [ ] Yes
- [x] No
Related Links (Optional)
Add any relevant references:
Make HttpHeaders strings const
Primarily to help make these strings usable in compile-time contexts, e.g. as annotation parameters. The MayBeConstant suppression makes me think there might be a reason for them not being const already, but I couldn't find any reasoning for that besides ABI compatibility.
Created from PR ktor/5237
Documentation for Run HttpStatement.execute on the engine's dispatcher
Description
To improve Dev UX we switch dispatcher to the engine's dispatcher for HttpStatement.execute { ... } and HttpStatement.body { ... } blocks.
Previously, if these methods were run on the main dispatcher, this resulted in UI freezes.
Code example
Code from documentation:
client.prepareGet("https://httpbin.org/bytes/$fileSize").execute { httpResponse ->
val channel: ByteReadChannel = httpResponse.body()
var count = 0L
stream.use {
while (!channel.exhausted()) {
val chunk = channel.readRemaining(bufferSize)
count += chunk.remaining
chunk.transferTo(stream)
println("Received $count bytes from ${httpResponse.contentLength()}")
}
}
}
Before this change, users had to manually switch dispatcher using withContext:
client.prepareGet("https://httpbin.org/bytes/$fileSize").execute { httpResponse ->
withContext(Dispatchers.IO) {
// ...
}
}
Migration guide
Does this change require a migration guide for existing users?
Generally no, but:
- if a user switches dispatcher manually, it is not necessary anymore and can be removed
- the thread this block is executed on might change. For example, if a user changes the UI state directly from within
execute { ... }, this might stop working
Related Links (Optional)
Add any relevant references:
Documentation for Native engines should use Dispatchers.IO not Dispatchers.Unconfined
Description
Native engines (Darwin, WinHttp and Curl) now respect engine dispatcher setting. And use Dispatchers.IO by default. Previously Dispatcher.Unconfined has been used.
Code example
Example of configuring the engine dispatcher:
HttpClient(Curl) {
engine {
dispatcher = Dispatchers.IO
}
}
Migration guide
Does this change require a migration guide for existing users?
- [ ] Yes
- [x] No
I'd expect no migration required, but it might be important to know that native engines' behavior is aligned with the rest of the engines here now.
Documentation for Auth API key plugin
Description
This feature introduces a very basic form of authentication for the Ktor back end. For trusted networks, server-to-server communication is often made using shared keys. Collectively, this type of scheme is generally referred to "API Key" authentication, where secret keys are sent through a header value and verified on each request.
Code example
/**
* Minimal Ktor application with API Key authentication.
*/
fun Application.minimalExample() {
// key that will be used to authenticate requests
val expectedApiKey = "this-is-expected-key"
// principal for the app
data class AppPrincipal(val key: String)
// now we install authentication feature
install(Authentication) {
// and then api key provider
apiKey {
// set function that is used to verify request
validate { keyFromHeader ->
keyFromHeader
.takeIf { it == expectedApiKey }
?.let { AppPrincipal(it) }
}
}
}
routing {
authenticate {
get {
val p = call.principal<AppPrincipal>()!!
call.respondText("Key: ${p.key}")
}
}
}
}
PR https://github.com/ktorio/ktor/pull/5243
Migration guide
Does this change require a migration guide for existing users?
- [ ] Yes
- [x] No
Related Links (Optional)
Add any relevant references:
- https://swagger.io/docs/specification/v3_0/authentication/api-keys/
- https://www.baeldung.com/spring-boot-api-key-secret
Once the description is complete, please re-assign this issue to a Technical Writer.
Documentation for Partial HTML response
Description
This feature introduces a new extension function for responding with partial HTML (i.e., with root elements other than <html>). It affects the HTML DSL plugin, and is intended for use with HTMX responses.
Code example
See pull/5223/
Example:
get("/books.html") {
call.respondHtmlFragment {
div("books") {
for (book in library.books()) {
bookItem()
}
}
}
}
Migration guide
Does this change require a migration guide for existing users?
- [ ] Yes
- [x] No
Documentation for EngineMain: Support reading trust store settings from the configuration
Please provide the following details so the technical writing team can begin work on this issue.
Description
New fields can be added, which can be specified for the Ktor Server Trust Store using a Configuration file.
- ktor.security.ssl.trustStore
- ktor.security.ssl.trustStorePassword
- ktor.security.ssl.enabledProtocols
They need to be added here: https://ktor.io/docs/server-ssl.html#config-file
Code example
ktor {
security {
ssl {
keyStore = keystore.jks
keyAlias = sampleAlias
keyStorePassword = foobar
privateKeyPassword = foobar
// new
trustStore = truststore.jks
trustStorePassword = foobar
enabledProtocols = ["TLSv1.2", "TLSv1.3"]
}
}
}
Migration guide
Does this change require a migration guide for existing users?
- [ ] Yes
- [x] No
Related Links (Optional)
Add any relevant references:
Once the description is complete, please re-assign this issue to a Technical Writer.
Documentation for Add duplex streaming for OkHttpClient
Please provide the following details so the technical writing team can begin work on this issue.
Description
Duplex streaming for OkHttpClient
With regular HTTP calls, the request always completes sending before the response begins to be received. With duplex, the request and response may be interleaved. That is, request body bytes may be sent after response headers or body bytes have been received.
OkHttp isDuplex() documentation
New property in OkHttpConfig: duplexStreamingEnabled - if true allows clients to send and receive data simultaneously using bidirectional streaming (HTTP/2 only).
Code example
val client = HttpClient(OkHttp) {
engine {
duplexStreamingEnabled = true // new property
config {
protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE))
}
}
}
Migration guide
Does this change require a migration guide for existing users?
- [ ] Yes
- [x] No
Once the description is complete, please re-assign this issue to a Technical Writer.