Changelog 3.2 version
3.2.2
released 14th July 2025
Client
JS/WASM: response doesn't contain the Content-Length header in a browser
Please see branch ktor-content-length-bug for exact description and minimal project showing this bug.
The following table shows under which Ktor versions and targets the bug occurs.
Target | Ktor | 3.0.3 | 3.1.0 | 3.1.1 | 3.1.2 |
---|---|---|---|---|---|
jvmTest | CIO client | OK | OK | OK | OK |
jsTest | JsClient | OK | OK | OK | OK |
wasmJsTest | JsClient | OK | OK | OK | OK |
browser js | JsClient | OK | OK | NOK | NOK |
browser wasm | JsClient | OK | NOK | NOK | NOK |
For example, Ktor 3.1.0 JsClient run in browser
- works for js
- doesn't work for wasm
See attachements.
There are bugfixes for KTOR-7934 in the releasees 3.1.0 and 3.1.1.
Each time changed the error pattern in my table above. Coincidence or cause?
Darwin: The Content-Encoding header is removed since 3.0.3
We have a custom plugin that decrypts data sent back from the server. It expects a Content-Encoding header be present with specific value. Since KTOR 3.0.3 that header is removed on iOS. Android works fine. Relevant chat on Slack. This happens for a GET request - I have not tested POST or anything else.
testApplication: The `client.sse()` acts like a REST call and not a stream in test environment
For me it looks like your test only works, because incoming.collectIndexed()
is called after the call to the endpoint is completely finished. That means, after repeat(6)
has been finished in your application completely and the connection has been closed.
I would expect that I get the events already before the connection has been closed as in my /unlimitedEvents example.
But in this case the tests never returns anything because client.sse()
waits until the routes have finished it's processing before its trailing lambda (ClientSSESession.() -> Unit
) is called.
When calling client.sse()
outside of testApplication
and it calls a running application, outside of the test environment, it works as expected. I do get the server sent events even before the connection is closed.
Did I miss something?
It seems inconsistent to me, that client.sse()
behaves differently, when called within testApplication
or outside of it. Even acknowledging the fact that it gets special initialization from within testApplication
in the latter case.
Do I need any specific configuration to get my testUnlimitedEvents()
test to receive the events event before the connection has been closed?
Any help would be much appreciated! Thanks in advance!
Request builder block overrides HTTP method in specific request builders (get, post, etc)
I am using Ktor 3.1.3. Template didn't allow me to change the version
When using takeFrom()
to copy configuration from another request builder inside a specific HTTP method builder (like client.post {}
), the HTTP method gets overridden by the method from the source request, ignoring the explicitly chosen method builder.
Steps to Reproduce:
- Create a default request configuration:
private fun defaultRequest() = request {
expectSuccess = false
url {
takeFrom(baseUrl)
}
}
- Use
takeFrom()
inside a specific HTTP method builder:
client.post {
takeFrom(defaultRequest())
url {
appendPathSegments("api", "endpoint")
}
setBody(requestBody)
}
Expected Behavior: The request should be sent as a POST request since it's explicitly using the client.post {}
builder.
Actual Behavior: The request is sent as a GET request (the default method from the request {}
builder used in ). defaultRequest()
Workaround: Explicitly set the method after using takeFrom()
:
client.request {
takeFrom(defaultRequest())
method = HttpMethod.Post
// ... rest of configuration
}
Or avoid using takeFrom()
and configure manually:
client.post {
url {
takeFrom(baseUrl)
appendPathSegments("api", "endpoint")
}
expectSuccess = false
setBody(requestBody)
}
Impact: This behavior is counterintuitive and can lead to unexpected API failures, especially when developers assume that using client.post {}
guarantees a POST request will be made.
Suggested Fix: The takeFrom()
method should not override the HTTP method when used inside a specific method builder (post, get, put, delete, etc.). The method should be preserved from the outer builder context.
Server
CORS: server replies with the 405 status code on a preflight request when the plugin is installed in a route
To reproduce make the following request with the curl
to the server with the following code:
curl -v -X OPTIONS --header "Origin: https://example.com" --header "Access-Control-Request-Method: PUT" http://localhost:3333/test
embeddedServer(Netty, port = 3333) {
install(CallLogging)
routing {
install(CORS) {
anyHost()
allowMethod(HttpMethod.Put)
}
// options("test") { }
put("test") {
call.respond("Hello World")
}
}
}.start(true)
As a result, an unexpected 405 Method Not Allowed
status code is returned from the server.
The server replies with a valid preflight response if an option {}
route is present.
StatusPages: response headers of OutgoingContent aren't available in the status handlers
To reproduce run the following test:
@Test
fun test() = testApplication {
install(StatusPages) {
status(HttpStatusCode.Unauthorized) { call, _ ->
assertEquals("Basic realm=myRealm, charset=UTF-8", call.response.headers[HttpHeaders.WWWAuthenticate])
call.respond(HttpStatusCode.InternalServerError)
}
}
install(Authentication) {
basic {
realm = "myRealm"
validate {
null
}
}
}
routing {
authenticate {
get("/secret") {
call.respondText("Secret!")
}
}
}
client.get("/secret")
}
As a result, the test fails because the OutgoingContent
(which isn't a part of the call object) contains the WWW-Authenticate
header.
Config deserialization - default properties problem
Default properties in data classes cannot be omitted in the configuration entries.
This occurs with HOCON but works fine with YAML.
To reproduce:
@Test
fun `more test`() {
val content = """
sendgrid {
# baseUrl = "https://api.sendgrid.com/v3"
apiKey = ${'$'}{SENDGRID_API_KEY}
apiKey = "missing"
}
""".trimIndent()
println(parseConfig(content).property("sendgrid").getAs<Sendgrid>())
}
private fun parseConfig(content: String): HoconApplicationConfig =
HoconApplicationConfig(ConfigFactory.parseString(content))
@Serializable
data class Sendgrid(
val baseUrl: String = "https://api.sendgrid.com/v3",
val apiKey: String,
)
Note that the commented-out property causes the failure.
kotlinx.datetime is not available transitively in 3.2.1
This dependency used to be exposed by the ktor-server-default-headers
module, but it has been removed in v3.2.1
This is a breaking change for projects relied on this dependency transitively, so we should bring it back and remove it only in the next minor release.
Shared
SSE: Change the order of SSE field serialization: put `event` before `data`
Currently, the `data` field comes before the `event` field in the SSE payload. If the streaming parser used the `event` field as a discriminator to determine the deserializer, it must buffer the `data`. Moving `event` before `data` would allow streaming SSE parsers to avoid buffering SSE's `data` field and choosing the right deserializer before reading the data field. In the reference `event` field also comes before the `data`.
Proposed fix: https://github.com/ktorio/ktor/pull/4968
3.2.1
released 4th July 2025
Client
Flow invariant is violated since 3.2.0
Bug Description
When attempting to collect a Flow in a Ktor Route:
get("/") {
val response = flow {
emit("Hello")
}.toList()
call.respond(response.joinToString(", "))
}
...the call fails with the following error message:
java.lang.IllegalStateException: Flow invariant is violated:
Flow was collected in [io.ktor.server.engine.DefaultUncaughtExceptionHandler@3ee5fadc, CoroutineName(call-handler), io.ktor.server.netty.NettyDispatcher$CurrentContext@46079c50, CoroutineId(5), "call-handler#5":StandaloneCoroutine{Active}@1d5b93f8, io.ktor.server.application.ClassLoaderAwareContinuationInterceptor@36726457],
but emission happened in [io.ktor.server.engine.DefaultUncaughtExceptionHandler@3ee5fadc, CoroutineName(call-handler), io.ktor.server.netty.NettyDispatcher$CurrentContext@46079c50, CoroutineId(5), "call-handler#5":StandaloneCoroutine{Active}@1d5b93f8, io.ktor.server.application.ClassLoaderAwareContinuationInterceptor@36726457].
Please refer to 'flow' documentation or use 'flowOn' instead
at kotlinx.coroutines.flow.internal.SafeCollector_commonKt.checkContext(SafeCollector.common.kt:84)
at kotlinx.coroutines.flow.internal.SafeCollector.checkContext(SafeCollector.kt:132)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:109)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:82)
Context
This code works in Ktor 3.1.3. In my application, I am using Flows to return data from the JetBrains Exposed based services to the Ktor Route.
Reproducer
"Space characters in SimpleName" error when executing R8 mergeExtDex task with 3.2.0
After updating to 3.2.0 version of Ktor, I'm not able to build a release version of my app.
I get this error related to R8
> Task :app:mergeExtDexRelease FAILED
AGPBI: {"kind":"error","text":"com.android.tools.r8.internal.Jf: Space characters in SimpleName 'use streaming syntax' are not allowed prior to DEX version 040","sources":[{"file":"/Users/gdesantos/.gradle/caches/8.14/transforms/36e97280d1537b14bcb0e517d30fb7bf/transformed/jetified-ktor-client-core-jvm-3.2.0.jar"}],"tool":"D8"}
More info:
FAILURE: Build completed with 2 failures.
1: Task failed with an exception.
\-----------
\* What went wrong:
Execution failed for task ':app:mergeExtDexRelease'.
\> Could not resolve all files for configuration ':app:releaseRuntimeClasspath'.
\> Failed to transform ktor-client-core-jvm-3.2.0.jar (io.ktor:ktor-client-core-jvm:3.2.0) to match attributes {artifactType=android-dex, dexing-component-attributes=ComponentSpecificParameters(minSdkVersion=26, debuggable=true, enableCoreLibraryDesugaring=false, enableGlobalSynthetics=true, enableApiModeling=false, dependenciesClassesAreInstrumented=false, asmTransformComponent=null, useJacocoTransformInstrumentation=false, enableDesugaring=true, needsClasspath=false, useFullClasspath=false, componentIfUsingFullClasspath=null), org.gradle.category=library, org.gradle.jvm.environment=standard-jvm, org.gradle.libraryelements=jar, org.gradle.status=release, org.gradle.usage=java-runtime, org.jetbrains.kotlin.platform.type=jvm}.
\> Execution failed for DexingNoClasspathTransform: /Users/gdesantos/.gradle/caches/8.14/transforms/36e97280d1537b14bcb0e517d30fb7bf/transformed/jetified-ktor-client-core-jvm-3.2.0.jar.
\> Error while dexing.
\* Try:
\> Run with --info or --debug option to get more log output.
\> Run with --scan to get full insights.
\> Get more help at <https://help.gradle.org>.
\* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':app:mergeExtDexRelease'.
at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:38)
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: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.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:42)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:331)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:318)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:314)
at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:85)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:314)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:303)
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:48)
Caused by: org.gradle.api.internal.artifacts.ivyservice.TypedResolveException: Could not resolve all files for configuration ':app:releaseRuntimeClasspath'.
at org.gradle.api.internal.artifacts.ivyservice.resolveengine.artifact.ArtifactSetToFileCollectionFactory$NameBackedResolutionHost.consolidateFailures(ArtifactSetToFileCollectionFactory.java:195)
at org.gradle.api.internal.artifacts.configurations.ResolutionHost.rethrowFailuresAndReportProblems(ResolutionHost.java:75)
at org.gradle.api.internal.artifacts.configurations.ResolutionBackedFileCollection.maybeThrowResolutionFailures(ResolutionBackedFileCollection.java:86)
at org.gradle.api.internal.artifacts.configurations.ResolutionBackedFileCollection.visitContents(ResolutionBackedFileCollection.java:76)
at org.gradle.api.internal.file.AbstractFileCollection.visitStructure(AbstractFileCollection.java:361)
at org.gradle.api.internal.file.CompositeFileCollection.lambda$visitContents$0(CompositeFileCollection.java:112)
at org.gradle.api.internal.file.collections.UnpackingVisitor.add(UnpackingVisitor.java:66)
at org.gradle.api.internal.file.collections.UnpackingVisitor.add(UnpackingVisitor.java:99)
at org.gradle.api.internal.file.DefaultFileCollectionFactory$ResolvingFileCollection.visitChildren(DefaultFileCollectionFactory.java:306)
at org.gradle.api.internal.file.CompositeFileCollection.visitContents(CompositeFileCollection.java:112)
at org.gradle.api.internal.file.AbstractFileCollection.visitStructure(AbstractFileCollection.java:361)
at org.gradle.api.internal.file.CompositeFileCollection.lambda$visitContents$0(CompositeFileCollection.java:112)
at org.gradle.api.internal.file.collections.UnpackingVisitor.add(UnpackingVisitor.java:66)
at org.gradle.api.internal.file.collections.UnpackingVisitor.add(UnpackingVisitor.java:91)
at org.gradle.api.internal.file.DefaultFileCollectionFactory$ResolvingFileCollection.visitChildren(DefaultFileCollectionFactory.java:306)
at org.gradle.api.internal.file.CompositeFileCollection.visitContents(CompositeFileCollection.java:112)
at org.gradle.api.internal.file.AbstractFileCollection.visitStructure(AbstractFileCollection.java:361)
at org.gradle.api.internal.file.CompositeFileCollection.lambda$visitContents$0(CompositeFileCollection.java:112)
at org.gradle.api.internal.tasks.PropertyFileCollection.visitChildren(PropertyFileCollection.java:48)
at org.gradle.api.internal.file.CompositeFileCollection.visitContents(CompositeFileCollection.java:112)
at org.gradle.api.internal.file.AbstractFileCollection.visitStructure(AbstractFileCollection.java:361)
at org.gradle.internal.fingerprint.impl.DefaultFileCollectionSnapshotter.snapshot(DefaultFileCollectionSnapshotter.java:47)
at org.gradle.internal.execution.impl.DefaultInputFingerprinter$InputCollectingVisitor.visitInputFileProperty(DefaultInputFingerprinter.java:133)
at org.gradle.api.internal.tasks.execution.TaskExecution.visitRegularInputs(TaskExecution.java:324)
at org.gradle.internal.execution.impl.DefaultInputFingerprinter.fingerprintInputProperties(DefaultInputFingerprinter.java:63)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.captureExecutionStateWithOutputs(AbstractCaptureStateBeforeExecutionStep.java:109)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.lambda$captureExecutionState$0(AbstractCaptureStateBeforeExecutionStep.java:74)
at org.gradle.internal.execution.steps.BuildOperationStep$1.call(BuildOperationStep.java:37)
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.internal.execution.steps.BuildOperationStep.operation(BuildOperationStep.java:34)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.captureExecutionState(AbstractCaptureStateBeforeExecutionStep.java:69)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:62)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:43)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.executeWithNonEmptySources(AbstractSkipEmptyWorkStep.java:125)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:56)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:36)
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:289)
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:48)
at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:35)
at org.gradle.internal.execution.impl.DefaultExecutionEngine$1.execute(DefaultExecutionEngine.java:64)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:127)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:116)
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: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.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:42)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:331)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:318)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:314)
at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:85)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:314)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:303)
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:48)
Caused by: org.gradle.api.internal.artifacts.transform.TransformException: Failed to transform ktor-client-core-jvm-3.2.0.jar (io.ktor:ktor-client-core-jvm:3.2.0) to match attributes {artifactType=android-dex, dexing-component-attributes=ComponentSpecificParameters(minSdkVersion=26, debuggable=true, enableCoreLibraryDesugaring=false, enableGlobalSynthetics=true, enableApiModeling=false, dependenciesClassesAreInstrumented=false, asmTransformComponent=null, useJacocoTransformInstrumentation=false, enableDesugaring=true, needsClasspath=false, useFullClasspath=false, componentIfUsingFullClasspath=null), org.gradle.category=library, org.gradle.jvm.environment=standard-jvm, org.gradle.libraryelements=jar, org.gradle.status=release, org.gradle.usage=java-runtime, org.jetbrains.kotlin.platform.type=jvm}.
at org.gradle.api.internal.artifacts.transform.TransformingAsyncArtifactListener$TransformedArtifact.lambda$visit$4(TransformingAsyncArtifactListener.java:242)
at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:293)
at org.gradle.api.internal.artifacts.transform.TransformingAsyncArtifactListener$TransformedArtifact.visit(TransformingAsyncArtifactListener.java:234)
at org.gradle.api.internal.artifacts.ivyservice.resolveengine.artifact.ParallelResolveArtifactSet$VisitingSet$StartVisitAction.visitResults(ParallelResolveArtifactSet.java:100)
at org.gradle.api.internal.artifacts.ivyservice.resolveengine.artifact.ParallelResolveArtifactSet$VisitingSet.visit(ParallelResolveArtifactSet.java:69)
at org.gradle.api.internal.artifacts.ivyservice.resolveengine.artifact.ArtifactSetToFileCollectionFactory$PartialSelectedArtifactSet.visitFiles(ArtifactSetToFileCollectionFactory.java:276)
at org.gradle.api.internal.artifacts.configurations.ResolutionBackedFileCollection.visitContents(ResolutionBackedFileCollection.java:75)
at org.gradle.api.internal.file.AbstractFileCollection.visitStructure(AbstractFileCollection.java:361)
at org.gradle.api.internal.file.CompositeFileCollection.lambda$visitContents$0(CompositeFileCollection.java:112)
at org.gradle.api.internal.file.collections.UnpackingVisitor.add(UnpackingVisitor.java:66)
at org.gradle.api.internal.file.collections.UnpackingVisitor.add(UnpackingVisitor.java:99)
at org.gradle.api.internal.file.DefaultFileCollectionFactory$ResolvingFileCollection.visitChildren(DefaultFileCollectionFactory.java:306)
at org.gradle.api.internal.file.CompositeFileCollection.visitContents(CompositeFileCollection.java:112)
at org.gradle.api.internal.file.AbstractFileCollection.visitStructure(AbstractFileCollection.java:361)
at org.gradle.api.internal.file.CompositeFileCollection.lambda$visitContents$0(CompositeFileCollection.java:112)
at org.gradle.api.internal.file.collections.UnpackingVisitor.add(UnpackingVisitor.java:66)
at org.gradle.api.internal.file.collections.UnpackingVisitor.add(UnpackingVisitor.java:91)
at org.gradle.api.internal.file.DefaultFileCollectionFactory$ResolvingFileCollection.visitChildren(DefaultFileCollectionFactory.java:306)
at org.gradle.api.internal.file.CompositeFileCollection.visitContents(CompositeFileCollection.java:112)
at org.gradle.api.internal.file.AbstractFileCollection.visitStructure(AbstractFileCollection.java:361)
at org.gradle.api.internal.file.CompositeFileCollection.lambda$visitContents$0(CompositeFileCollection.java:112)
at org.gradle.api.internal.tasks.PropertyFileCollection.visitChildren(PropertyFileCollection.java:48)
at org.gradle.api.internal.file.CompositeFileCollection.visitContents(CompositeFileCollection.java:112)
at org.gradle.api.internal.file.AbstractFileCollection.visitStructure(AbstractFileCollection.java:361)
at org.gradle.internal.fingerprint.impl.DefaultFileCollectionSnapshotter.snapshot(DefaultFileCollectionSnapshotter.java:47)
at org.gradle.internal.execution.impl.DefaultInputFingerprinter$InputCollectingVisitor.visitInputFileProperty(DefaultInputFingerprinter.java:133)
at org.gradle.api.internal.tasks.execution.TaskExecution.visitRegularInputs(TaskExecution.java:324)
at org.gradle.internal.execution.impl.DefaultInputFingerprinter.fingerprintInputProperties(DefaultInputFingerprinter.java:63)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.captureExecutionStateWithOutputs(AbstractCaptureStateBeforeExecutionStep.java:109)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.lambda$captureExecutionState$0(AbstractCaptureStateBeforeExecutionStep.java:74)
at org.gradle.internal.execution.steps.BuildOperationStep$1.call(BuildOperationStep.java:37)
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.internal.execution.steps.BuildOperationStep.operation(BuildOperationStep.java:34)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.captureExecutionState(AbstractCaptureStateBeforeExecutionStep.java:69)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:62)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:43)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.executeWithNonEmptySources(AbstractSkipEmptyWorkStep.java:125)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:56)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:36)
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:289)
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:48)
at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:35)
at org.gradle.internal.execution.impl.DefaultExecutionEngine$1.execute(DefaultExecutionEngine.java:64)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:127)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:116)
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: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.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:42)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:331)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:318)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:314)
at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:85)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:314)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:303)
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:48)
Caused by: org.gradle.api.internal.artifacts.transform.TransformException: Execution failed for DexingNoClasspathTransform: /Users/gdesantos/.gradle/caches/8.14/transforms/36e97280d1537b14bcb0e517d30fb7bf/transformed/jetified-ktor-client-core-jvm-3.2.0.jar.
at org.gradle.api.internal.artifacts.transform.DefaultTransformInvocationFactory.lambda$createInvocation$1(DefaultTransformInvocationFactory.java:167)
at org.gradle.internal.Try$Failure.mapFailure(Try.java:284)
at org.gradle.api.internal.artifacts.transform.DefaultTransformInvocationFactory.lambda$createInvocation$2(DefaultTransformInvocationFactory.java:167)
at org.gradle.internal.Deferrable$1.applyAndRequireNonNull(Deferrable.java:63)
at org.gradle.internal.Deferrable$1.completeAndGet(Deferrable.java:59)
at org.gradle.internal.Deferrable$1.completeAndGet(Deferrable.java:59)
at org.gradle.internal.Deferrable.lambda$flatMap$0(Deferrable.java:79)
at org.gradle.internal.Deferrable$3.completeAndGet(Deferrable.java:117)
at org.gradle.api.internal.artifacts.transform.TransformingAsyncArtifactListener$TransformedArtifact.finalizeValue(TransformingAsyncArtifactListener.java:208)
at org.gradle.api.internal.artifacts.transform.TransformingAsyncArtifactListener$TransformedArtifact.run(TransformingAsyncArtifactListener.java:146)
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.DefaultBuildOperationExecutor$QueueWorker.execute(DefaultBuildOperationExecutor.java:161)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.runOperation(DefaultBuildOperationQueue.java:272)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.doRunBatch(DefaultBuildOperationQueue.java:253)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.lambda$runBatch$0(DefaultBuildOperationQueue.java:238)
at org.gradle.internal.resources.AbstractResourceLockRegistry.whileDisallowingLockChanges(AbstractResourceLockRegistry.java:50)
at org.gradle.internal.work.DefaultWorkerLeaseService.whileDisallowingProjectLockChanges(DefaultWorkerLeaseService.java:235)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.lambda$runBatch$1(DefaultBuildOperationQueue.java:238)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.runBatch(DefaultBuildOperationQueue.java:224)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.run(DefaultBuildOperationQueue.java:192)
... 2 more
Caused by: com.android.builder.dexing.DexArchiveBuilderException: Error while dexing.
at com.android.builder.dexing.D8DexArchiveBuilder.getExceptionToRethrow(D8DexArchiveBuilder.java:192)
at com.android.builder.dexing.D8DexArchiveBuilder.convert(D8DexArchiveBuilder.java:131)
at com.android.build.gradle.internal.dependency.BaseDexingTransform.process(DexingTransform.kt:292)
at com.android.build.gradle.internal.dependency.BaseDexingTransform.processNonIncrementally(DexingTransform.kt:238)
at com.android.build.gradle.internal.dependency.BaseDexingTransform.transform(DexingTransform.kt:150)
at org.gradle.api.internal.artifacts.transform.DefaultTransform.transform(DefaultTransform.java:282)
at org.gradle.api.internal.artifacts.transform.AbstractTransformExecution$2.call(AbstractTransformExecution.java:154)
at org.gradle.api.internal.artifacts.transform.AbstractTransformExecution$2.call(AbstractTransformExecution.java:145)
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.api.internal.artifacts.transform.AbstractTransformExecution.executeWithinTransformerListener(AbstractTransformExecution.java:145)
at org.gradle.api.internal.artifacts.transform.AbstractTransformExecution.execute(AbstractTransformExecution.java:138)
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: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.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.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:61)
at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:26)
at org.gradle.internal.execution.steps.NoInputChangesStep.execute(NoInputChangesStep.java:30)
at org.gradle.internal.execution.steps.NoInputChangesStep.execute(NoInputChangesStep.java:21)
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.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:175)
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:115)
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.NeverUpToDateStep.execute(NeverUpToDateStep.java:34)
at org.gradle.internal.execution.steps.NeverUpToDateStep.execute(NeverUpToDateStep.java:22)
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.ResolveNonIncrementalCachingStateStep.executeDelegate(ResolveNonIncrementalCachingStateStep.java:50)
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.ValidateStep.execute(ValidateStep.java:107)
at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:56)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:64)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:43)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:38)
at org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.lambda$executeInTemporaryWorkspace$3(AssignImmutableWorkspaceStep.java:209)
at org.gradle.internal.execution.workspace.impl.CacheBasedImmutableWorkspaceProvider$1.withTemporaryWorkspace(CacheBasedImmutableWorkspaceProvider.java:116)
at org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.executeInTemporaryWorkspace(AssignImmutableWorkspaceStep.java:199)
at org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.lambda$execute$0(AssignImmutableWorkspaceStep.java:121)
at org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.execute(AssignImmutableWorkspaceStep.java:121)
at org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.execute(AssignImmutableWorkspaceStep.java:90)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:38)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:23)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.lambda$execute$0(ExecuteWorkBuildOperationFiringStep.java:53)
at org.gradle.internal.execution.steps.BuildOperationStep$1.call(BuildOperationStep.java:37)
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.internal.execution.steps.BuildOperationStep.operation(BuildOperationStep.java:34)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.lambda$execute$1(ExecuteWorkBuildOperationFiringStep.java:51)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:51)
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.executeInCache(IdentityCacheStep.java:80)
at org.gradle.internal.execution.steps.IdentityCacheStep.lambda$executeDeferred$0(IdentityCacheStep.java:60)
at org.gradle.cache.Cache.lambda$get$0(Cache.java:31)
at org.gradle.cache.ManualEvictionInMemoryCache.get(ManualEvictionInMemoryCache.java:30)
at org.gradle.cache.internal.DefaultCrossBuildInMemoryCacheFactory$CrossBuildCacheRetainingDataFromPreviousBuild.get(DefaultCrossBuildInMemoryCacheFactory.java:303)
at org.gradle.cache.Cache.get(Cache.java:31)
at org.gradle.internal.execution.steps.IdentityCacheStep.lambda$executeDeferred$1(IdentityCacheStep.java:58)
at org.gradle.internal.Deferrable$3.completeAndGet(Deferrable.java:117)
... 23 more
Caused by: com.android.tools.r8.CompilationFailedException: Compilation failed to complete, origin: /Users/gdesantos/.gradle/caches/8.14/transforms/36e97280d1537b14bcb0e517d30fb7bf/transformed/jetified-ktor-client-core-jvm-3.2.0.jar:io/ktor/client/plugins/Messages.class
at Version.fakeStackEntry(Version_8.10.24.java:0)
at com.android.tools.r8.Q.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:5)
at com.android.tools.r8.internal.nv.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:82)
at com.android.tools.r8.internal.nv.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:32)
at com.android.tools.r8.internal.nv.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:31)
at com.android.tools.r8.internal.nv.b(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:2)
at com.android.tools.r8.D8.run(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:11)
at com.android.builder.dexing.D8DexArchiveBuilder.convert(D8DexArchiveBuilder.java:129)
... 115 more
Caused by: com.android.tools.r8.internal.Jf: Space characters in SimpleName 'use streaming syntax' are not allowed prior to DEX version 040
at com.android.tools.r8.graph.j4.d(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:8)
at com.android.tools.r8.graph.j4.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:136)
at com.android.tools.r8.internal.Wd.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:630)
at com.android.tools.r8.graph.o4.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:36)
at com.android.tools.r8.internal.nv.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:134)
at com.android.tools.r8.internal.nv.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:135)
at com.android.tools.r8.internal.nv.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:133)
at com.android.tools.r8.graph.o4.b(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:2)
at com.android.tools.r8.dex.a.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:31)
at com.android.tools.r8.internal.zr0.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:23)
at com.google.common.util.concurrent.TrustedListenableFutureTask$TrustedFutureInterruptibleTask.runInterruptibly(TrustedListenableFutureTask.java:131)
at com.google.common.util.concurrent.InterruptibleTask.run(InterruptibleTask.java:76)
at com.google.common.util.concurrent.TrustedListenableFutureTask.run(TrustedListenableFutureTask.java:82)
at com.google.common.util.concurrent.DirectExecutorService.execute(DirectExecutorService.java:51)
at com.google.common.util.concurrent.AbstractListeningExecutorService.submit(AbstractListeningExecutorService.java:79)
at com.google.common.util.concurrent.AbstractListeningExecutorService.submit(AbstractListeningExecutorService.java:37)
at com.android.tools.r8.internal.Nr0.submit(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:1)
at com.android.tools.r8.internal.zr0.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:15)
at com.android.tools.r8.internal.zr0.b(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:1)
at com.android.tools.r8.dex.a.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:29)
at com.android.tools.r8.dex.a.b(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:136)
at com.android.tools.r8.dex.c.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:38)
at com.android.tools.r8.dex.c.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:12)
at com.android.tools.r8.dex.c.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:9)
at com.android.tools.r8.D8.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:33)
at com.android.tools.r8.D8.d(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:20)
at com.android.tools.r8.D8.b(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:1)
at com.android.tools.r8.internal.nv.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:28)
... 118 more
Suppressed: java.lang.RuntimeException: java.util.concurrent.ExecutionException: com.android.tools.r8.internal.pv: com.android.tools.r8.internal.Jf: Space characters in SimpleName 'use streaming syntax' are not allowed prior to DEX version 040
at com.android.tools.r8.dex.c.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:60)
at com.android.tools.r8.dex.c.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:12)
at com.android.tools.r8.dex.c.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:9)
at com.android.tools.r8.D8.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:33)
at com.android.tools.r8.D8.d(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:20)
at com.android.tools.r8.D8.b(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:1)
at com.android.tools.r8.internal.nv.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:28)
at com.android.tools.r8.internal.nv.b(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:2)
at com.android.tools.r8.D8.run(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:11)
at com.android.builder.dexing.D8DexArchiveBuilder.convert(D8DexArchiveBuilder.java:129)
at com.android.build.gradle.internal.dependency.BaseDexingTransform.process(DexingTransform.kt:292)
at com.android.build.gradle.internal.dependency.BaseDexingTransform.processNonIncrementally(DexingTransform.kt:238)
at com.android.build.gradle.internal.dependency.BaseDexingTransform.transform(DexingTransform.kt:150)
at org.gradle.api.internal.artifacts.transform.DefaultTransform.transform(DefaultTransform.java:282)
at org.gradle.api.internal.artifacts.transform.AbstractTransformExecution$2.call(AbstractTransformExecution.java:154)
at org.gradle.api.internal.artifacts.transform.AbstractTransformExecution$2.call(AbstractTransformExecution.java:145)
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.api.internal.artifacts.transform.AbstractTransformExecution.executeWithinTransformerListener(AbstractTransformExecution.java:145)
at org.gradle.api.internal.artifacts.transform.AbstractTransformExecution.execute(AbstractTransformExecution.java:138)
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: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.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.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:61)
at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:26)
at org.gradle.internal.execution.steps.NoInputChangesStep.execute(NoInputChangesStep.java:30)
at org.gradle.internal.execution.steps.NoInputChangesStep.execute(NoInputChangesStep.java:21)
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.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 java.base/java.util.Optional.orElseGet(Optional.java:364)
at org.gradle.internal.execution.steps.BuildCacheStep.lambda$executeWithCache$5(BuildCacheStep.java:101)
at org.gradle.internal.Try$Success.map(Try.java:175)
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:115)
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.NeverUpToDateStep.execute(NeverUpToDateStep.java:34)
at org.gradle.internal.execution.steps.NeverUpToDateStep.execute(NeverUpToDateStep.java:22)
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.ResolveNonIncrementalCachingStateStep.executeDelegate(ResolveNonIncrementalCachingStateStep.java:50)
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.ValidateStep.execute(ValidateStep.java:107)
at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:56)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:64)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:43)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:38)
at org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.lambda$executeInTemporaryWorkspace$3(AssignImmutableWorkspaceStep.java:209)
at org.gradle.internal.execution.workspace.impl.CacheBasedImmutableWorkspaceProvider$1.withTemporaryWorkspace(CacheBasedImmutableWorkspaceProvider.java:116)
at org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.executeInTemporaryWorkspace(AssignImmutableWorkspaceStep.java:199)
at org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.lambda$execute$0(AssignImmutableWorkspaceStep.java:121)
at java.base/java.util.Optional.orElseGet(Optional.java:364)
at org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.execute(AssignImmutableWorkspaceStep.java:121)
at org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.execute(AssignImmutableWorkspaceStep.java:90)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:38)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:23)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.lambda$execute$0(ExecuteWorkBuildOperationFiringStep.java:53)
at org.gradle.internal.execution.steps.BuildOperationStep$1.call(BuildOperationStep.java:37)
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.internal.execution.steps.BuildOperationStep.operation(BuildOperationStep.java:34)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.lambda$execute$1(ExecuteWorkBuildOperationFiringStep.java:51)
at java.base/java.util.Optional.map(Optional.java:260)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:51)
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.executeInCache(IdentityCacheStep.java:80)
at org.gradle.internal.execution.steps.IdentityCacheStep.lambda$executeDeferred$0(IdentityCacheStep.java:60)
at org.gradle.cache.Cache.lambda$get$0(Cache.java:31)
at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
at org.gradle.cache.ManualEvictionInMemoryCache.get(ManualEvictionInMemoryCache.java:30)
at org.gradle.cache.internal.DefaultCrossBuildInMemoryCacheFactory$CrossBuildCacheRetainingDataFromPreviousBuild.get(DefaultCrossBuildInMemoryCacheFactory.java:303)
at org.gradle.cache.Cache.get(Cache.java:31)
at org.gradle.internal.execution.steps.IdentityCacheStep.lambda$executeDeferred$1(IdentityCacheStep.java:58)
at org.gradle.internal.Deferrable$3.completeAndGet(Deferrable.java:117)
at org.gradle.internal.Deferrable$1.completeAndGet(Deferrable.java:59)
at org.gradle.internal.Deferrable$1.completeAndGet(Deferrable.java:59)
at org.gradle.internal.Deferrable.lambda$flatMap$0(Deferrable.java:79)
at org.gradle.internal.Deferrable$3.completeAndGet(Deferrable.java:117)
at org.gradle.api.internal.artifacts.transform.TransformingAsyncArtifactListener$TransformedArtifact.finalizeValue(TransformingAsyncArtifactListener.java:208)
at org.gradle.api.internal.artifacts.transform.TransformingAsyncArtifactListener$TransformedArtifact.run(TransformingAsyncArtifactListener.java:146)
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.DefaultBuildOperationExecutor$QueueWorker.execute(DefaultBuildOperationExecutor.java:161)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.runOperation(DefaultBuildOperationQueue.java:272)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.doRunBatch(DefaultBuildOperationQueue.java:253)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.lambda$runBatch$0(DefaultBuildOperationQueue.java:238)
at org.gradle.internal.resources.AbstractResourceLockRegistry.whileDisallowingLockChanges(AbstractResourceLockRegistry.java:50)
at org.gradle.internal.work.DefaultWorkerLeaseService.whileDisallowingProjectLockChanges(DefaultWorkerLeaseService.java:235)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.lambda$runBatch$1(DefaultBuildOperationQueue.java:238)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.runBatch(DefaultBuildOperationQueue.java:224)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.run(DefaultBuildOperationQueue.java:192)
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.util.concurrent.ExecutionException: com.android.tools.r8.internal.pv: com.android.tools.r8.internal.Jf: Space characters in SimpleName 'use streaming syntax' are not allowed prior to DEX version 040
at com.google.common.util.concurrent.AbstractFuture.getDoneValue(AbstractFuture.java:596)
at com.google.common.util.concurrent.AbstractFuture.get(AbstractFuture.java:555)
at com.google.common.util.concurrent.FluentFuture$TrustedFuture.get(FluentFuture.java:91)
at com.android.tools.r8.internal.Nr0.forEach(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:4)
at com.android.tools.r8.internal.Nr0.awaitFutures(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:1)
at com.android.tools.r8.internal.zr0.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:16)
at com.android.tools.r8.dex.c.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:39)
... 131 more
Caused by: com.android.tools.r8.internal.pv: com.android.tools.r8.internal.Jf: Space characters in SimpleName 'use streaming syntax' are not allowed prior to DEX version 040
at com.android.tools.r8.internal.nv.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:155)
at com.android.tools.r8.internal.nv.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:133)
at com.android.tools.r8.graph.o4.b(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:2)
at com.android.tools.r8.dex.a.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:31)
at com.android.tools.r8.internal.zr0.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:23)
at com.google.common.util.concurrent.TrustedListenableFutureTask$TrustedFutureInterruptibleTask.runInterruptibly(TrustedListenableFutureTask.java:131)
at com.google.common.util.concurrent.InterruptibleTask.run(InterruptibleTask.java:76)
at com.google.common.util.concurrent.TrustedListenableFutureTask.run(TrustedListenableFutureTask.java:82)
at com.google.common.util.concurrent.DirectExecutorService.execute(DirectExecutorService.java:51)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at com.google.common.util.concurrent.AbstractListeningExecutorService.submit(AbstractListeningExecutorService.java:79)
at com.google.common.util.concurrent.AbstractListeningExecutorService.submit(AbstractListeningExecutorService.java:37)
at com.android.tools.r8.internal.Nr0.submit(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:1)
at com.android.tools.r8.internal.zr0.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:15)
at com.android.tools.r8.internal.zr0.b(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:1)
at com.android.tools.r8.dex.a.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:29)
at com.android.tools.r8.dex.a.b(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:136)
at com.android.tools.r8.dex.c.a(R8_8.10.24_9e9057dad18d37eb780da811fe3614733dcf9651f84e198486f77e150f7678a4:38)
... 131 more
Caused by: [CIRCULAR REFERENCE: com.android.tools.r8.internal.Jf: Space characters in SimpleName 'use streaming syntax' are not allowed prior to DEX version 040]
OkHttp: java.net.ProtocolException when sending MultiPartFormDataContent with onUpload
I have performed an upload, with a code like this:
override fun uploadFirmware(
byteArray: ByteArray,
fileName: String
): Flow<Pair<Boolean?, Int>> = callbackFlow {
val resultData = client.post("update") {
url {
headers {
append(HttpHeaders.AcceptEncoding, "gzip, deflate, brl")
append(HttpHeaders.Accept, "*/*")
}
}
onUpload { bytesSentTotal, contentLength ->
val progressPercentage =
(bytesSentTotal.toDouble() / contentLength.toDouble()) * 100
trySend(null to progressPercentage.toInt())
}
setBody(
MultiPartFormDataContent(
formData {
append(
key = "firmware",
value = byteArray,
Headers.build {
append(HttpHeaders.ContentType, "multipart/*")
append(HttpHeaders.ContentDisposition, "filename=\"$fileName\"")
}
)
}
)
)
}
trySend(resultData.status.isSuccess() to 200)
awaitClose {
close()
}
}
When I use onUpload, I encounter the error java.net.ProtocolException: unexpected end of stream. However, if I don't use onUpload, all processes run smoothly.
I noticed a difference in contentLength between the ByteArray I sent and the one in onUpload. ByteArray contentLength: 1446128, onUpload contentLength: 1446424.
Infrastructure
Publish Javadoc as a maven artifact
Currently, the javadoc/dokka output is not published as a maven artifact.
The javadoc jar published to maven central looks like this:
ktor-server-core-1.6.7-javadoc/
└── META-INF
└── MANIFEST.MF
1 directory, 1 file
It would be great it you could publish the output of dokka to maven, rather than just to the api.ktor.io site.
Network
Potential race condition in `socket.awaitClosed` (hangs indefinitely) since 3.2.0
We noticed a potential race condition happening in ASocket.awaitClosed, causing 1 out of 100ish threads calling the function to hang indefinitely
I have created a test like this:
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
class OpenCloseConnectionsTest : FunSpec(
{
test("Repeatedly opening and closing connections should never hang the thread") {
val address = InetSocketAddress.createUnresolved("localhost", 9999)
val selectorManager = SelectorManager(newSingleThreadContext("TcpRunner"))
aSocket(selectorManager)
.tcp()
.bind(address.hostName, address.port)
.use { serverSocket ->
println("TCP server is listening at ${serverSocket.localAddress}")
repeat(5_000) {
// If the thread hangs, this won't complete within the timeout
println("Awaiting new connection")
val socketAsync = async { serverSocket.accept() }
aSocket(selectorManager).tcp().connect(address.hostName, address.port)
val socket = socketAsync.await()
println("Accepted connection from ${socket.remoteAddress}, $socket")
println("Closing connection to ${socket.remoteAddress}")
val thread = thread {
val readChannel = socket.openReadChannel()
val writeChannel = socket.openWriteChannel(autoFlush = true)
socket.close()
runBlocking {
// With manual flushAndClose of the WriteChannel, 3.2.0 works.
//
// Note: This is _usually_ not needed, only a small percentage of connections
// fail to close properly without it.
// writeChannel.flushAndClose()
socket.awaitClosed()
}
}
thread.join(Duration.ofMillis(500))
thread.state shouldBe Thread.State.TERMINATED // Fails, 1 in ~100-150ish threads get stuck as TIMED_WAITING indefinetly
}
}
}
},
)
This test fails using Ktor-network 3.2.0, but passes on version 3.1.3
Server
Replace kotlinx.datetime APIs with kotlin.time
Starting with kotlinx-datetime 0.7.0, some types have been removed: Instant, Clock
We should use stdlib types instead.
Usages of kotlinx-datetime APIs prevent users from updating to newer versions of kotlinx-datetime.
A workaround is available
GitHub issue: https://github.com/ktorio/ktor/issues/4969
ForwardedHeaders: the plugin does not handle parameters case-insensitively
According to RFC 7239, the parameters must be case-insensitive:
The parameter names are case-insensitive.
they further elaborate with the following examples:
Forwarded: for="_gazonk"
Forwarded: For="[2001:db8:cafe::17]:4711"
Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43
Forwarded: for=192.0.2.43, for=198.51.100.17
A very simple fix to this would be to perform the following change in ForwardedHeaders.kt
:
-val map = value.params.associateByTo(HashMap(), { it.name }, { it.value })
+val map = value.params.associateByTo(HashMap(), { it.name.lowercase() }, { it.value })
Module parameter type Application.() -> kotlin.Unit is not supported in 3.2.0
Everything was working fine in 3.1.x and since upgrade to 3.2.0 our applications do not start anymore due to this:
Exception in thread "main" java.lang.IllegalArgumentException: Parameter type io.ktor.server.application.Application.() -> kotlin.Unit:{null} is not supported.Application is loaded as class io.ktor.server.application.Application:{jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7}
at io.ktor.server.engine.internal.CallableUtilsKt.callFunctionWithInjection(CallableUtils.kt:113)
at io.ktor.server.engine.internal.CallableUtilsKt.executeModuleFunction(CallableUtils.kt:42)
at io.ktor.server.engine.EmbeddedServer$launchModuleByName$2.invokeSuspend(EmbeddedServerJvm.kt:443)
at io.ktor.server.engine.EmbeddedServer$launchModuleByName$2.invoke(EmbeddedServerJvm.kt)
at io.ktor.server.engine.EmbeddedServer$launchModuleByName$2.invoke(EmbeddedServerJvm.kt)
at io.ktor.server.engine.EmbeddedServer.avoidingDoubleStartupFor(EmbeddedServerJvm.kt:469)
at io.ktor.server.engine.EmbeddedServer.launchModuleByName(EmbeddedServerJvm.kt:442)
at io.ktor.server.engine.EmbeddedServer.access$launchModuleByName(EmbeddedServerJvm.kt:33)
at io.ktor.server.engine.EmbeddedServer$dynamicModule$1.invokeSuspend(EmbeddedServerJvm.kt:402)
at io.ktor.server.engine.EmbeddedServer$dynamicModule$1.invoke(EmbeddedServerJvm.kt)
at io.ktor.server.engine.EmbeddedServer$dynamicModule$1.invoke(EmbeddedServerJvm.kt)
at io.ktor.server.application.ApplicationModules_jvmKt$LoadSequentially$1.loadModules(ApplicationModules.jvm.kt:75)
at io.ktor.server.engine.EmbeddedServer$instantiateAndConfigureApplication$1$1.invokeSuspend(EmbeddedServerJvm.kt:385)
at io.ktor.server.engine.EmbeddedServer$instantiateAndConfigureApplication$1$1.invoke(EmbeddedServerJvm.kt)
at io.ktor.server.engine.EmbeddedServer$instantiateAndConfigureApplication$1$1.invoke(EmbeddedServerJvm.kt)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndspatched(Undispatched.kt:66)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturnIgnoreTimeout(Undispatched.kt:50)
at kotlinx.coroutines.TimeoutKt.setupTimeout(Timeout.kt:149)
at kotlinx.coroutines.TimeoutKt.withTimeout(Timeout.kt:44)
at kotlinx.coroutines.TimeoutKt.withTimeout-KLykuaI(Timeout.kt:72)
at io.ktor.server.engine.EmbeddedServer$instantiateAndConfigureApplication$1.invokeSuspend(EmbeddedServerJvm.kt:384)
at io.ktor.server.engine.EmbeddedServer$instantiateAndConfigureApplication$1.invoke(EmbeddedServerJvm.kt)
at io.ktor.server.engine.EmbeddedServer$instantiateAndConfigureApplication$1.invoke(EmbeddedServerJvm.kt)
at io.ktor.server.engine.EmbeddedServer$avoidingDoubleStartup$1.invokeSuspend(EmbeddedServerJvm.kt:450)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:263)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:94)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:70)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:48)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at io.ktor.server.engine.EmbeddedServer.avoidingDoubleStartup(EmbeddedServerJvm.kt:449)
at io.ktor.server.engine.EmbeddedServer.instantiateAndConfigureApplication(EmbeddedServerJvm.kt:383)
at io.ktor.server.engine.EmbeddedServer.createApplication(EmbeddedServerJvm.kt:169)
at io.ktor.server.engine.EmbeddedServer.start(EmbeddedServerJvm.kt:314)
at io.ktor.server.netty.EngineMain.main(EngineMain.kt:25)
Problem is that the official documentation on the KTOR website is not up-to-date with 3.2.0 so we don't have any info on changes affecting this.
We use the configuration file mode instead of embedded server, like:
fun main(args: Array<String>) {
io.ktor.server.netty.EngineMain.main(args)
}
With references to several Ktor modules in the `application.conf` file.
I don't really know where to start here. What's happening ?
Thanks in advance
OAuth2 authentication provider breaks form-urlencoded POST requests when receiving request body
Hi,
Ktor 2.0.0 now supports receiving OAuth code responses as form post (cf. KTOR-3342). However, this seems to break requests to OAuth secured routes with application/x-www-form-urlencoded
content, since the call is already consumed by receiveParameters()
in OAuth2.kt#L66
.
Consider the following test:
class ApplicationTest {
@Test
fun testWithOauth() = testApplication {
application {
install(Authentication) {
oauth("auth-oauth-dummy") {
urlProvider = { "http://localhost:8080/callback" }
providerLookup = {
OAuthServerSettings.OAuth2ServerSettings(
name = "dummy",
authorizeUrl = "localhost",
accessTokenUrl = "localhost",
clientId = "clientId",
clientSecret = "clientSecret"
)
}
client = HttpClient(Apache)
}
}
routing {
route("/") {
post {
val contentType = call.request.contentType()
val payload = call.receiveText()
call.respond("POST / - $contentType - $payload")
}
}
route("/oauth") {
authenticate("auth-oauth-dummy", optional = true) {
post {
val contentType = call.request.contentType()
try {
val payload = call.receiveText()
call.respond("POST /oauth - $contentType - $payload")
} catch (t: Throwable) {
call.respond(HttpStatusCode.InternalServerError, "POST /oauth - $contentType - ${t.message}")
}
}
}
}
}
}
// Works
client.post("/") {
setBody(TextContent(listOf("foo" to "bar").formUrlEncode(), ContentType.Application.FormUrlEncoded))
}.apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals("POST / - application/x-www-form-urlencoded - foo=bar", bodyAsText())
}
// Fails
// status is `500`
// bodyAsText() is `POST /oauth - application/x-www-form-urlencoded - Request body has already been consumed (received).`
client.post("/oauth") {
setBody(TextContent(listOf("foo" to "bar").formUrlEncode(), ContentType.Application.FormUrlEncoded))
}.apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals("POST /oauth - application/x-www-form-urlencoded - foo=bar", bodyAsText())
}
}
}
The test is simplified and my actual case is that users can skip the OAuth flow when they already provide a valid JWT. It is working with Ktor 1.6.8, though.
404 for a link in KDoc for io.ktor.server.plugins.contentnegotiation.ContentNegotiation
"You can learn more from Content negotiation and serialization." leads to a 404 page. Correct URL is https://ktor.io/docs/server-serialization.html
Ktor fails to boot with default jvminline argument
Using the standard setup to generate a ktor app and then adding a default parameter to the module
function as seen below, and running ./gradlew run
it fails to boot with this error:
Task :run FAILED
2025-06-23 12:20:56.938 [main] INFO Application - Autoreload is disabled because the development mode is off.
Exception in thread "main" io.ktor.server.engine.internal.ReloadingException: Module function cannot be found for the fully qualified name 'com.example.ApplicationKt.module'
fun Application.module(boot: BasicValue1 = BasicValue1("boot")) {
configureRouting()
}
@JvmInline
value class BasicValue1(val initials: String)
data class BasicValue2(val initials: String)
When switching the parameter to use BasicValue2
which is not a JvmInline class, then it works without any issues.
Tested both in 3.2
and 2.3
Netty: Invalid hex byte with malformed query string
Hello All,
A get request with a malformed query string causes a 500 error with the message
invalid hex byte 's%' at index 3 of '/?%s%'
Steps to reproduce,
- Start a ktor server.
- Navigate to http://localhost:2000/?%s%
- Server than returns 500
Expected Behavior
Server returns 400 Bad Request
ResponseSent hook handler of the plugin installed into a route isn't executed when an exception is thrown from the route
In case of an error route scoped plugins ignore ResponseSent hook. It can be reproduced with the following example
fun main() {
embeddedServer(Netty, port = 5000, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
fun Application.module() {
routing {
install(CallLogging)
route("/") {
handle {
throw BadRequestException("Here comes the error")
}
}
}
}
@KtorDsl
class Config
val CallLogging: RouteScopedPlugin<Config> =
createRouteScopedPlugin("MicrometerMetrics", ::Config) {
on(CallFailed) { call, e ->
LoggerFactory.getLogger("Test").error("Call with error ${call.response.status()}", e)
}
on(ResponseSent) { call ->
LoggerFactory.getLogger("Test").info("Call ended ${call.response.status()}")
}
}
Expected behaviour is two error messages but in fact there is only one
Other
Thymeleaf: null values in template model
Currently, it is not possible to pass null values in the template model for thymeleaf.
For example:
call.respondTemplate(
"my-template.html",
mapOf(
"foo" to "foo",
"bar" to null,
)
)
A current workaround for this issue is to do an unchecked cast on the map to erase the nullability type information:
@Suppress("UNCHECKED_CAST")
call.respondTemplate(
"my-template.html",
mapOf(
"foo" to "foo",
"bar" to null,
) as Map<String, Any>
)
ThymeleafContent
should be changed so it can accept a model
of type Map<String, Any?>
rather than Map<String, Any>
(& respondTemplate
should be updated accordingly)
3.2.0
released 13th June 2025
Client
SaveBodyPlugin: Logging plugin consumes response body
To reproduce run the following code:
@Serializable
data class ErrorResponse(val error: String)
@Serializable
data class OKResponse(val data: String)
suspend fun main() {
val client = HttpClient(CIO) {
install(Logging) {
level = LogLevel.ALL
}
install(SaveBodyPlugin) {
}
install(ContentNegotiation) {
json()
}
HttpResponseValidator {
validateResponse { response ->
val contentType = response.contentType()
if (contentType?.match(ContentType.Application.Json) == true) {
try {
val errorResponse: ErrorResponse = response.body()
throw RuntimeException(errorResponse.error)
} catch (e: ContentConvertException) {
// no need to handle
}
}
}
}
}
val response = client.get("http://0.0.0.0:4444")
println(response.body<OKResponse>())
}
The server:
embeddedServer(Netty, port = 4444) {
routing {
get {
call.respondText(ContentType.Application.Json) { """{"data": "test"}""" }
}
}
}.start(wait = true)
As a result, an unexpected ContentConvertException
is thrown. The above code works as expected without the Logging
plugin with level = LogLevel.ALL
.
HttpCache: Support evicting/clearing cache
I've implemented the Cache
plugin and the requests are correctly cached and respect the caching headers
Our use case sends the request with and without bearer tokens - and a different response is sent if it contains a valid bearer token
The issue being is that the cache is respected even with the addition of a bearer token, and thus not showing the logged in response
I've checked the API for the cache and there are only methods to find
and store
information on the cache, it would be really useful to be able to purge this cache in some way!
Ability to use browser cookie storage
This issue was imported from GitHub issue: https://github.com/ktorio/ktor/issues/1403
Subsystem
ktor-client-js
Is your feature request related to a problem? Please describe.
I need to be able to store cookies between sessions in a browser.
Describe the solution you'd like
A cookie store that uses the browser's cookie storage. Possibly by simply adding credentials: "include"
to the fetch request.
Motivation to include to ktor
A common requirement in web applications is to use the browser's cookie storage (so e.g. authentication cookies can be preserved between sessions). By default the client seems to use an in-memory cookie store and there seems to only be a constant cookie store alternative.
Logging/Darwin: IOException is thrown when detecting if body is a binary
I'm developing a KMP&Compose app.
While making network calls using Ktorfit and Ktor. It's using Ktor 3.1.2 (as Ktorfit is built with it), with the HttpClient set up as such:
HttpClient(get<HttpClientEngine>()) { //get<HttpClientEngine>() provides Darwin engine for iOS
install(Logging) {
logger = Logger.SIMPLE
level = LogLevel.BODY
format = LoggingFormat.OkHttp
sanitizeHeader { header -> header == HttpHeaders.Authorization }
}
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
prettyPrint = true
})
}
}
Running the app on iPhone 15 Pro running on iOS 18.5, Exception is being thrown:
kotlinx.io.IOException: Max argument is deprecated
at 0 composeApp.debug.dylib 0x1083b6edb kfun:kotlin.Throwable#<init>(kotlin.String?){} + 99
at 1 composeApp.debug.dylib 0x1083b0b9f kfun:kotlin.Exception#<init>(kotlin.String?){} + 95
at 2 composeApp.debug.dylib 0x1092a3e0b kfun:kotlinx.io.IOException#<init>(kotlin.String?){} + 95
at 3 composeApp.debug.dylib 0x1092be7fb kfun:io.ktor.utils.io.charsets#decode__at__io.ktor.utils.io.charsets.CharsetDecoder(kotlinx.io.Source;kotlin.text.Appendable;kotlin.Int){}kotlin.Int + 555
at 4 composeApp.debug.dylib 0x1092bc077 kfun:io.ktor.utils.io.charsets#decode__at__io.ktor.utils.io.charsets.CharsetDecoder(kotlinx.io.Source;kotlin.Int){}kotlin.String + 503
at 5 composeApp.debug.dylib 0x10939bd5f kfun:io.ktor.client.plugins.logging.Logging$1.$invoke$detectIfBinaryCOROUTINE$8.invokeSuspend#internal + 2027
at 6 composeApp.debug.dylib 0x10939c537 kfun:io.ktor.client.plugins.logging.Logging$1.invoke$detectIfBinary#internal + 399
at 7 composeApp.debug.dylib 0x10939caef kfun:io.ktor.client.plugins.logging.Logging$1.$invoke$logRequestBodyCOROUTINE$9.invokeSuspend#internal + 951
at 8 composeApp.debug.dylib 0x10939d6a3 kfun:io.ktor.client.plugins.logging.Logging$1.invoke$logRequestBody#internal + 471
at 9 composeApp.debug.dylib 0x10939dceb kfun:io.ktor.client.plugins.logging.Logging$1.$invoke$logOutgoingContentCOROUTINE$10.invokeSuspend#internal + 1151
at 10 composeApp.debug.dylib 0x10939e94f kfun:io.ktor.client.plugins.logging.Logging$1.invoke$logOutgoingContent#internal + 423
at 11 composeApp.debug.dylib 0x10939eb0f kfun:io.ktor.client.plugins.logging.Logging$1.invoke$logOutgoingContent$default#internal + 375
at 12 composeApp.debug.dylib 0x1093a0bc7 kfun:io.ktor.client.plugins.logging.Logging$1.$invoke$logRequestOkHttpFormatCOROUTINE$11.invokeSuspend#internal + 7979
at 13 composeApp.debug.dylib 0x1093a0ed7 kfun:io.ktor.client.plugins.logging.Logging$1.invoke$logRequestOkHttpFormat#internal + 399
at 14 composeApp.debug.dylib 0x1093a625b kfun:io.ktor.client.plugins.logging.Logging$1.Logging$1$invoke$1.$invokeCOROUTINE$0.invokeSuspend#internal + 1163
at 15 composeApp.debug.dylib 0x1093a6cd3 kfun:io.ktor.client.plugins.logging.Logging$1.Logging$1$invoke$1.invoke#internal + 335
at 16 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 17 composeApp.debug.dylib 0x10939949f kfun:io.ktor.client.plugins.logging.SendHook.SendHook$install$1.invoke#internal + 323
at 18 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 19 composeApp.debug.dylib 0x1092e631f kfun:io.ktor.util.pipeline.DebugPipelineContext.$proceedLoopCOROUTINE$0.invokeSuspend#internal + 723
at 20 composeApp.debug.dylib 0x1092e653f kfun:io.ktor.util.pipeline.DebugPipelineContext.proceedLoop#internal + 275
at 21 composeApp.debug.dylib 0x1092e5e97 kfun:io.ktor.util.pipeline.DebugPipelineContext#proceed#suspend(kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 351
at 22 composeApp.debug.dylib 0x1092e5f83 kfun:io.ktor.util.pipeline.DebugPipelineContext#execute#suspend(1:0;kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 167
at 23 composeApp.debug.dylib 0x109520657 kfun:io.ktor.util.pipeline.PipelineContext#execute#suspend(1:0;kotlin.coroutines.Continuation<1:0>){}kotlin.Any-trampoline + 75
at 24 composeApp.debug.dylib 0x1092e8b7b kfun:io.ktor.util.pipeline.Pipeline#execute#suspend(1:1;1:0;kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 311
at 25 composeApp.debug.dylib 0x10937617b kfun:io.ktor.client.plugins.HttpSend.DefaultSender.$executeCOROUTINE$1.invokeSuspend#internal + 1187
at 26 composeApp.debug.dylib 0x109376533 kfun:io.ktor.client.plugins.HttpSend.DefaultSender.execute#internal + 299
at 27 composeApp.debug.dylib 0x109527d87 kfun:io.ktor.client.plugins.Sender#execute#suspend(io.ktor.client.request.HttpRequestBuilder;kotlin.coroutines.Continuation<io.ktor.client.call.HttpClientCall>){}kotlin.Any-trampoline + 115
at 28 composeApp.debug.dylib 0x10937beb7 kfun:io.ktor.client.plugins.api.Send.Sender#proceed#suspend(io.ktor.client.request.HttpRequestBuilder;kotlin.coroutines.Continuation<io.ktor.client.call.HttpClientCall>){}kotlin.Any + 199
at 29 composeApp.debug.dylib 0x109371bd7 kfun:io.ktor.client.plugins.HttpRedirect$1.HttpRedirect$1$invoke$1.$invokeCOROUTINE$0.invokeSuspend#internal + 471
at 30 composeApp.debug.dylib 0x1093720cb kfun:io.ktor.client.plugins.HttpRedirect$1.HttpRedirect$1$invoke$1.invoke#internal + 335
at 31 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 32 composeApp.debug.dylib 0x10937c23f kfun:io.ktor.client.plugins.api.Send.Send$install$1.invoke#internal + 351
at 33 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 34 composeApp.debug.dylib 0x1093758c3 kfun:io.ktor.client.plugins.HttpSend.InterceptedSender.execute#internal + 243
at 35 composeApp.debug.dylib 0x109527d87 kfun:io.ktor.client.plugins.Sender#execute#suspend(io.ktor.client.request.HttpRequestBuilder;kotlin.coroutines.Continuation<io.ktor.client.call.HttpClientCall>){}kotlin.Any-trampoline + 115
at 36 composeApp.debug.dylib 0x10937beb7 kfun:io.ktor.client.plugins.api.Send.Sender#proceed#suspend(io.ktor.client.request.HttpRequestBuilder;kotlin.coroutines.Continuation<io.ktor.client.call.HttpClientCall>){}kotlin.Any + 199
at 37 composeApp.debug.dylib 0x10936a377 kfun:io.ktor.client.plugins.HttpCallValidator$1.HttpCallValidator$1$invoke$2.$invokeCOROUTINE$2.invokeSuspend#internal + 471
at 38 composeApp.debug.dylib 0x10936a7a7 kfun:io.ktor.client.plugins.HttpCallValidator$1.HttpCallValidator$1$invoke$2.invoke#internal + 335
at 39 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 40 composeApp.debug.dylib 0x10937c23f kfun:io.ktor.client.plugins.api.Send.Send$install$1.invoke#internal + 351
at 41 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 42 composeApp.debug.dylib 0x1093758c3 kfun:io.ktor.client.plugins.HttpSend.InterceptedSender.execute#internal + 243
at 43 composeApp.debug.dylib 0x109527d87 kfun:io.ktor.client.plugins.Sender#execute#suspend(io.ktor.client.request.HttpRequestBuilder;kotlin.coroutines.Continuation<io.ktor.client.call.HttpClientCall>){}kotlin.Any-trampoline + 115
at 44 composeApp.debug.dylib 0x109375257 kfun:io.ktor.client.plugins.HttpSend.Plugin.HttpSend$Plugin$install$1.$invokeCOROUTINE$0.invokeSuspend#internal + 2511
at 45 composeApp.debug.dylib 0x10937560b kfun:io.ktor.client.plugins.HttpSend.Plugin.HttpSend$Plugin$install$1.invoke#internal + 335
at 46 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 47 composeApp.debug.dylib 0x1092e631f kfun:io.ktor.util.pipeline.DebugPipelineContext.$proceedLoopCOROUTINE$0.invokeSuspend#internal + 723
at 48 composeApp.debug.dylib 0x1092e653f kfun:io.ktor.util.pipeline.DebugPipelineContext.proceedLoop#internal + 275
at 49 composeApp.debug.dylib 0x1092e5e97 kfun:io.ktor.util.pipeline.DebugPipelineContext#proceed#suspend(kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 351
at 50 composeApp.debug.dylib 0x1092e5d0f kfun:io.ktor.util.pipeline.DebugPipelineContext#proceedWith#suspend(1:0;kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 147
at 51 composeApp.debug.dylib 0x109520827 kfun:io.ktor.util.pipeline.PipelineContext#proceedWith#suspend(1:0;kotlin.coroutines.Continuation<1:0>){}kotlin.Any-trampoline + 75
at 52 composeApp.debug.dylib 0x10936179b kfun:io.ktor.client.plugins.defaultTransformers$1.$invokeCOROUTINE$0.invokeSuspend#internal + 2491
at 53 composeApp.debug.dylib 0x109361a07 kfun:io.ktor.client.plugins.defaultTransformers$1.invoke#internal + 335
at 54 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 55 composeApp.debug.dylib 0x1092e631f kfun:io.ktor.util.pipeline.DebugPipelineContext.$proceedLoopCOROUTINE$0.invokeSuspend#internal + 723
at 56 composeApp.debug.dylib 0x1092e653f kfun:io.ktor.util.pipeline.DebugPipelineContext.proceedLoop#internal + 275
at 57 composeApp.debug.dylib 0x1092e5e97 kfun:io.ktor.util.pipeline.DebugPipelineContext#proceed#suspend(kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 351
at 58 composeApp.debug.dylib 0x1092e5d0f kfun:io.ktor.util.pipeline.DebugPipelineContext#proceedWith#suspend(1:0;kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 147
at 59 composeApp.debug.dylib 0x109520827 kfun:io.ktor.util.pipeline.PipelineContext#proceedWith#suspend(1:0;kotlin.coroutines.Continuation<1:0>){}kotlin.Any-trampoline + 75
at 60 composeApp.debug.dylib 0x10937da5f kfun:io.ktor.client.plugins.api.TransformRequestBodyHook.TransformRequestBodyHook$install$1.$invokeCOROUTINE$0.invokeSuspend#internal + 1023
at 61 composeApp.debug.dylib 0x10937dcef kfun:io.ktor.client.plugins.api.TransformRequestBodyHook.TransformRequestBodyHook$install$1.invoke#internal + 335
at 62 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 63 composeApp.debug.dylib 0x1092e631f kfun:io.ktor.util.pipeline.DebugPipelineContext.$proceedLoopCOROUTINE$0.invokeSuspend#internal + 723
at 64 composeApp.debug.dylib 0x1092e653f kfun:io.ktor.util.pipeline.DebugPipelineContext.proceedLoop#internal + 275
at 65 composeApp.debug.dylib 0x1092e5e97 kfun:io.ktor.util.pipeline.DebugPipelineContext#proceed#suspend(kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 351
at 66 composeApp.debug.dylib 0x10952089f kfun:io.ktor.util.pipeline.PipelineContext#proceed#suspend(kotlin.coroutines.Continuation<1:0>){}kotlin.Any-trampoline + 67
at 67 composeApp.debug.dylib 0x109367633 kfun:io.ktor.client.plugins.RequestError.RequestError$install$1.$invokeCOROUTINE$0.invokeSuspend#internal + 451
at 68 composeApp.debug.dylib 0x109367aa7 kfun:io.ktor.client.plugins.RequestError.RequestError$install$1.invoke#internal + 335
at 69 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 70 composeApp.debug.dylib 0x1092e631f kfun:io.ktor.util.pipeline.DebugPipelineContext.$proceedLoopCOROUTINE$0.invokeSuspend#internal + 723
at 71 composeApp.debug.dylib 0x1092e653f kfun:io.ktor.util.pipeline.DebugPipelineContext.proceedLoop#internal + 275
at 72 composeApp.debug.dylib 0x1092e5e97 kfun:io.ktor.util.pipeline.DebugPipelineContext#proceed#suspend(kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 351
at 73 composeApp.debug.dylib 0x10952089f kfun:io.ktor.util.pipeline.PipelineContext#proceed#suspend(kotlin.coroutines.Continuation<1:0>){}kotlin.Any-trampoline + 67
at 74 composeApp.debug.dylib 0x10937297b kfun:io.ktor.client.plugins.SetupRequestContext.SetupRequestContext$install$1.SetupRequestContext$install$1$invoke$$FUNCTION_REFERENCE_FOR$proceed$0.$invokeCOROUTINE$0.invokeSuspend#internal + 391
at 75 composeApp.debug.dylib 0x109372ba7 kfun:io.ktor.client.plugins.SetupRequestContext.SetupRequestContext$install$1.SetupRequestContext$install$1$invoke$$FUNCTION_REFERENCE_FOR$proceed$0.invoke#internal + 275
at 76 composeApp.debug.dylib 0x109463533 kfun:kotlin.coroutines.SuspendFunction0#invoke#suspend(kotlin.coroutines.Continuation<1:0>){}kotlin.Any?-trampoline + 107
at 77 composeApp.debug.dylib 0x10937347b kfun:io.ktor.client.plugins.HttpRequestLifecycle$1.HttpRequestLifecycle$1$invoke$1.$invokeCOROUTINE$1.invokeSuspend#internal + 883
at 78 composeApp.debug.dylib 0x1093737f3 kfun:io.ktor.client.plugins.HttpRequestLifecycle$1.HttpRequestLifecycle$1$invoke$1.invoke#internal + 335
at 79 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 80 composeApp.debug.dylib 0x109372683 kfun:io.ktor.client.plugins.SetupRequestContext.SetupRequestContext$install$1.invoke#internal + 311
at 81 composeApp.debug.dylib 0x10946344f kfun:kotlin.coroutines.SuspendFunction2#invoke#suspend(1:0;1:1;kotlin.coroutines.Continuation<1:2>){}kotlin.Any?-trampoline + 123
at 82 composeApp.debug.dylib 0x1092e631f kfun:io.ktor.util.pipeline.DebugPipelineContext.$proceedLoopCOROUTINE$0.invokeSuspend#internal + 723
at 83 composeApp.debug.dylib 0x1092e653f kfun:io.ktor.util.pipeline.DebugPipelineContext.proceedLoop#internal + 275
at 84 composeApp.debug.dylib 0x1092e5e97 kfun:io.ktor.util.pipeline.DebugPipelineContext#proceed#suspend(kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 351
at 85 composeApp.debug.dylib 0x1092e5f83 kfun:io.ktor.util.pipeline.DebugPipelineContext#execute#suspend(1:0;kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 167
at 86 composeApp.debug.dylib 0x109520657 kfun:io.ktor.util.pipeline.PipelineContext#execute#suspend(1:0;kotlin.coroutines.Continuation<1:0>){}kotlin.Any-trampoline + 75
at 87 composeApp.debug.dylib 0x1092e8b7b kfun:io.ktor.util.pipeline.Pipeline#execute#suspend(1:1;1:0;kotlin.coroutines.Continuation<1:0>){}kotlin.Any + 311
at 88 composeApp.debug.dylib 0x10934c81f kfun:io.ktor.client.HttpClient.$executeCOROUTINE$2.invokeSuspend#internal + 567
at 89 composeApp.debug.dylib 0x10934cab7 kfun:io.ktor.client.HttpClient#execute#suspend(io.ktor.client.request.HttpRequestBuilder;kotlin.coroutines.Continuation<io.ktor.client.call.HttpClientCall>){}kotlin.Any + 299
at 90 composeApp.debug.dylib 0x10938eb13 kfun:io.ktor.client.statement.HttpStatement.$fetchResponseCOROUTINE$4.invokeSuspend#internal + 807
at 91 composeApp.debug.dylib 0x10938f16f kfun:io.ktor.client.statement.HttpStatement#fetchResponse#suspend(kotlin.coroutines.Continuation<io.ktor.client.statement.HttpResponse>){}kotlin.Any + 275
at 92 composeApp.debug.dylib 0x10938e723 kfun:io.ktor.client.statement.HttpStatement#execute#suspend(kotlin.coroutines.Continuation<io.ktor.client.statement.HttpResponse>){}kotlin.Any + 103
at 93 composeApp.debug.dylib 0x1093b8db3 kfun:de.jensklingenberg.ktorfit.internal.KtorfitConverterHelper.$suspendRequestCOROUTINE$1.invokeSuspend#internal + 1859
at 94 composeApp.debug.dylib 0x1093b92fb kfun:de.jensklingenberg.ktorfit.internal.KtorfitConverterHelper#suspendRequest#suspend(de.jensklingenberg.ktorfit.converter.TypeData;kotlin.Function1<io.ktor.client.request.HttpRequestBuilder,kotlin.Unit>;kotlin.coroutines.Continuation<0:0?>){0§<kotlin.Any?>}kotlin.Any? + 335
at 95 composeApp.debug.dylib 0x10941ff2f kfun:com.composeApp.login.network.api._LoginApiImpl.$loginCOROUTINE$0.invokeSuspend#internal + 739
at 96 composeApp.debug.dylib 0x1094201e7 kfun:com.composeApp.login.network.api._LoginApiImpl#login#suspend(com.composeApp.login.network.model.UserLoginDto;kotlin.coroutines.Continuation<io.ktor.client.statement.HttpResponse>){}kotlin.Any + 299
at 97 composeApp.debug.dylib 0x10952f13f kfun:com.composeApp.login.network.api.LoginApi#login#suspend(com.composeApp.login.network.model.UserLoginDto;kotlin.coroutines.Continuation<io.ktor.client.statement.HttpResponse>){}kotlin.Any-trampoline + 115
at 98 composeApp.debug.dylib 0x109430fe3 kfun:com.composeApp.login.viewmodel.LoginRepository.$loginCOROUTINE$0.invokeSuspend#internal + 1151
at 99 composeApp.debug.dylib 0x109431627 kfun:com.composeApp.login.viewmodel.LoginRepository#login#suspend(kotlin.String;kotlin.String;kotlin.coroutines.Continuation<kotlin.Boolean>){}kotlin.Any + 335
at 100 composeApp.debug.dylib 0x109433923 kfun:com.composeApp.login.viewmodel.LoginViewModel.LoginViewModel$login$1.$invokeCOROUTINE$0.invokeSuspend#internal + 1127
at 101 composeApp.debug.dylib 0x10943406b kfun:com.composeApp.login.viewmodel.LoginViewModel.LoginViewModel$login$1.invoke#internal + 299
at 102 composeApp.debug.dylib 0x10946317b kfun:kotlin.Function2#invoke(1:0;1:1){}1:2-trampoline + 115
at 103 composeApp.debug.dylib 0x1083c0757 kfun:kotlin.coroutines.intrinsics.createCoroutineUnintercepted$$inlined$createCoroutineFromSuspendFunction$2.invokeSuspend#internal + 871
at 104 composeApp.debug.dylib 0x10946298f kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any?-trampoline + 67
at 105 composeApp.debug.dylib 0x1083bcfdb kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 691
at 106 composeApp.debug.dylib 0x109462a6f kfun:kotlin.coroutines.Continuation#resumeWith(kotlin.Result<1:0>){}-trampoline + 99
at 107 composeApp.debug.dylib 0x10856ba3f kfun:kotlinx.coroutines.DispatchedTask#run(){} + 2039
at 108 composeApp.debug.dylib 0x109478c1f kfun:kotlinx.coroutines.Runnable#run(){}-trampoline + 91
at 109 composeApp.debug.dylib 0x108596a43 kfun:kotlinx.coroutines.DarwinGlobalQueueDispatcher.DarwinGlobalQueueDispatcher$dispatch$$inlined$autoreleasepool$1.invoke#internal + 71
at 110 composeApp.debug.dylib 0x108596a9f kfun:kotlinx.coroutines.DarwinGlobalQueueDispatcher.DarwinGlobalQueueDispatcher$dispatch$$inlined$autoreleasepool$1.$<bridge-DNN>invoke(){}#internal + 71
at 111 composeApp.debug.dylib 0x109460bbf kfun:kotlin.Function0#invoke(){}1:0-trampoline + 99
at 112 composeApp.debug.dylib 0x1085987db _6f72672e6a6574627261696e732e6b6f746c696e783a6b6f746c696e782d636f726f7574696e65732d636f72652f6f70742f6275696c644167656e742f776f726b2f343465633665383530643563363366302f6b6f746c696e782d636f726f7574696e65732d636f72652f6e617469766544617277696e2f7372632f44697370617463686572732e6b74_knbridge4 + 191
at 113 libdispatch.dylib 0x190a70aab <redacted> + 31
at 114 libdispatch.dylib 0x190a8a583 <redacted> + 15
at 115 libdispatch.dylib 0x190aa6287 <redacted> + 31
at 116 libdispatch.dylib 0x190a751f7 <redacted> + 847
at 117 libdispatch.dylib 0x190a82daf <redacted> + 363
at 118 libdispatch.dylib 0x190a8354b <redacted> + 155
at 119 libsystem_pthread.dylib 0x21320c9cf _pthread_wqthread + 231
at 120 libsystem_pthread.dylib 0x21320caab start_wqthread + 7
The above setup works just fine running on Android (with the OkHttp platform engine).
The stacktrace points to this lines in Logging client plugin class:
val firstChunk = ByteArray(1024)
val firstReadSize = body.readAvailable(firstChunk)
if (firstReadSize < 1) {
return Triple(false, 0L, body)
}
val buffer = Buffer().apply { writeFully(firstChunk, 0, firstReadSize) }
val firstChunkText = charset.newDecoder().decode(buffer, firstReadSize)
which in the end calls the CharsetDecoder inside CharsetDarwin file:
@Suppress("CAST_NEVER_SUCCEEDS")
@OptIn(UnsafeNumber::class, BetaInteropApi::class)
public actual fun CharsetDecoder.decode(input: Source, dst: Appendable, max: Int): Int {
if (max != Int.MAX_VALUE) {
throw IOException("Max argument is deprecated")
}
val charset = _charset as? CharsetDarwin ?: error("Charset $this is not supported by darwin.")
val source: ByteArray = input.readByteArray()
val data = source.toNSData()
val content = NSString.create(data, charset.encoding) as? String
?: throw MalformedInputException("Failed to convert Bytes to String using $charset")
dst.append(content)
return content.length
}
I managed to get it working by creating a copy of the Logging
plugin copy on the project side, by simply not passing firstReadSize
to the decode()
function which defaults the max
argument to Int.MAX_VALUE
:
val firstChunkText = charset.newDecoder().decode(buffer)
Not sure if that qualifies as a proper fix as it's only tested on a single, rather simple network call.
Below are the libraries versions used in the project, taken from the Version Catalog:
composeMultiplatform = "1.8.1"
junit = "4.13.2"
ksp = "2.1.20-2.0.0"
kotlin = "2.1.20"
serializationPlugin = "2.1.20"
kotlinxSerialization = "1.8.1"
koin-bom = "4.0.3"
koinCompiler = "2.0.0"
ktor = "3.1.2"
coroutines = "1.10.2"
logback = "1.4.11"
napier = "2.7.1"
ktorfit = "2.5.2"
Deprecate SaveBodyPlugin in favor of HttpClientCall.save
We call HttpClientCall.save
for every non-streaming call in HttpStatement
. We save response body to release resources and simplify the management of the HttpResponse
lifecycle.
The response body of the saved call can be read more than once as it is cached in memory.
The only problem with this is that the save
is called outside the request pipeline, making plugins like HttpRequestRetry
unaware of any errors occurred during response body reading.
On the other hand, we have SaveBodyPlugin
which also allows reading body many times. It would be logical to replace the functionality of this plugin by moving call HttpClientCall.save
into the pipeline.
Proposed solution
- Deprecate
SaveBodyPlugin
andHttpRequestBuilder.skipSavingBody
with a warning. - Introduce a new internal plugin
SaveBody
callingHttpClientCall.save
and releasing resources for non-streaming responses afterHttpSend
and before all other plugins.
Migration Impact
- The new plugin will be installed by default instead of
SaveBodyPlugin
- Explicit installation of
SaveBodyPlugin
will not take any effect but show deprecation warnings - The behavior will remain the same both for non-streaming responses (body is saved automatically) and streaming responses (body is not saved)
- Response body reading and saving will take place at the stage
Before
of the response pipeline instead of reading it after pipeline execution.
This might affect plugins monitoring the response byte channel (like BodyProgress) as they should add monitoring before the response is saved (if we need them working for non-streaming responses).
Linux curl engine doesn't work for simultaneous websocket and http request
Description
Hi there, I'm working on a project: https://github.com/HackWebRTC/kmp-socketio, KMP (pure Kotlin) implementation of SocketIO client, which works for most platforms but linux.
I recently know that since 3.1.0, linux curl engine supports websocket, but when I try to add linux support, I encounter a problem. It seems that linux curl engine doesn't work for simultaneous websocket and http request.
I prepared a reproduce test case in my repo.
Code:
The minimal reproduce test case is: https://github.com/HackWebRTC/kmp-socketio/blob/linux_support/kmp-socketio/src/linuxTest/kotlin/com/piasy/kmp/socketio/socketio/ConnectionTestLinux.kt
In the @Before function, it will start a simple socket io server in localhost, so we can connect to it in the test case like below:
val config: HttpClientConfig<*>.() -> Unit = {
install(io.ktor.client.plugins.logging.Logging) {
logger = object : Logger {
override fun log(message: String) {
Logging.info("Net", message)
}
}
level = LogLevel.ALL
}
install(WebSockets) {
pingIntervalMillis = 20_000
}
}
val client = HttpClient(Curl) {
config(this)
}
client.webSocket("ws://localhost:3000/socket.io/?EIO=4&transport=websocket", {}) {
Logging.info(TAG, "sent ws request")
while (true) {
try {
val frame = incoming.receive()
Logging.info(TAG, "Receive frame: $frame")
val resp = client.request("http://localhost:3000/socket.io/?EIO=4&transport=polling") {
this.method = HttpMethod.Get
}
Logging.info(TAG, "http response status: ${resp.status}")
if (resp.status.isSuccess()) {
val body = resp.bodyAsText()
Logging.info(TAG, "http response body: $body")
}
} catch (e: Exception) {
Logging.info(TAG, "Receive error while reading websocket frame: `${e.message}`")
break
}
}
}
Steps to reproduce
- git clone -b linux_support https://github.com/HackWebRTC/kmp-socketio.git
- cd kmp-socketio/kmp-socketio/src/jvmTest/resources
- npm install
- cd ../../../..
- ./gradlew :kmp-socketio:linuxX64Test --info
On the test code above, when a websocket connection is established, and a frame is read, then if I start a http request, it will block forever.
ktor-network produces ProGuard warning
Description:
Prerequisites: A JVM project with ProGuard plugin applied. It can be a KMP project with desktop JVM target.
- Add ktor-network as a dependency
- Run ProGuard obfuscation task proguardReleaseJars
The task fails with:
Warning: io.ktor.network.sockets.SocketBase$attachFor$1: can't find enclosing method 'io.ktor.utils.io.ChannelJob attachFor(java.lang.String,io.ktor.utils.io.ByteChannel,kotlinx.atomicfu.AtomicRef,kotlin.jvm.functions.Function0)' in program class io.ktor.network.sockets.SocketBase
See attached project archive.
Android: "ProtocolException: TRACE does not support writing" when sending TRACE request
When sending a TRACE request via Ktor Client using the Android engine, without setting the request body, I get the following exception:
java.net.ProtocolException: TRACE does not support writing
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.initHttpEngine(HttpURLConnectionImpl.java:332)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:128)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getOutputStream(HttpURLConnectionImpl.java:262)
at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getOutputStream(DelegatingHttpsURLConnection.java:219)
at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:30)
at io.ktor.client.engine.android.AndroidClientEngine.execute(AndroidClientEngine.kt:81)
at io.ktor.client.engine.HttpClientEngine$executeWithinCallContext$2.invokeSuspend(HttpClientEngine.kt:183)
Having taken a glance at AndroidClientEngine.kt
, I get a hunch that adding TRACE to the METHODS_WITHOUT_BODY
list might solve the issue.
Reproduced using ktor-client-android:3.1.1
HttpClientCall: Deprecate `wrapWithContent` and `wrap`
The following extension methods are not compatible with SavedBodyPlugin
and can lead to behavior when a plugin consumes the response body (see KTOR-6474).
public fun HttpClientCall.wrapWithContent(content: ByteReadChannel): HttpClientCall
public fun HttpClientCall.wrap(content: ByteReadChannel, headers: Headers): HttpClientCall
The issue is that these methods replace potentially replayable ByteReadChannel
with a non-replayable one. When plugins access rawContent
multiple times, they get the same instance of ByteReadChannel
and they'll encounter errors since the channel can only be read once.
The proper way to wrap a call is to use the lambda-based variant that produces a new channel on each access to rawContent
:
// ❌ Non-replayable - decode called once, subsequent access to rawContent will fail
val decodedResponse = decode(response.rawContent)
call.wrapWithContent(decodedResponse)
// ✅ Replayable - decode will be called each time `rawContent` is accessed
call.wrapWithContent { decode(response.rawContent) }
Proposed solution
- Introduce a new API replacing error-prone
wrap
andwrapWithContent
public fun HttpClientCall.replaceResponse( headers: Headers = response.headers, content: HttpResponse.() -> ByteReadChannel, ): HttpClientCall
- Add deprecation messages directing users to use the new API
- Update KDocs to clarify the purpose of the lambdas
The "Content-Length: 0" header is added for GET requests sent to some servers
A call to httpClient.get(url)
will include the header "Content-Length: 0" in the request. I believe this should not be automatically added. According to RFC 9110:
A user agent SHOULD NOT send a Content-Length header field when the request message does not contain content and the method semantics do not anticipate such data.
I take this to mean that the "Content-Length: 0" header should not be included when request.method == HttpMethod.Get && content is NoContent
If we follow the robustness principle ("be conservative in what you send, be liberal in what you accept"), then I think we'd come to the conclusion that:
- this should not be a problem for any server
- but Ktor still shouldn't be including the header in this situation either
I could be wrong here. If so, please let me know the reasoning.
HttpRequestRetry: requests with some IOException's thrown by Java engine aren't retried
To reproduce run the following code:
val client = HttpClient(Java) {
install(HttpRequestRetry) {
retryOnExceptionIf(maxRetries = 3) { _, cause ->
cause is IOException
}
modifyRequest {
println("Retried...")
}
}
}
val r = client.get("http://localhost:5555")
println(r.bodyAsText())
Server:
val selectorManager = SelectorManager(Dispatchers.IO)
val server = aSocket(selectorManager).tcp().bind("127.0.0.1", 5555)
while (true) {
val socket = server.accept()
println("accepted connection")
launch {
val sendChannel = socket.openWriteChannel(autoFlush = true)
sendChannel.writeStringUtf8("""
HTTP/1.1 200 OK
content-type: application/json;charset=UTF-8
transfer-encoding: chunked
""".trimIndent().replace("\n", "\r\n"))
sendChannel.flush()
socket.close()
}
}
As result the following exception is thrown without the request being retried:
Exception in thread "main" java.io.IOException: chunked transfer encoding, state: READING_LENGTH
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
at io.ktor.utils.io.ExceptionUtilsJvmKt$createConstructor$$inlined$safeCtor$1.invoke(ExceptionUtilsJvm.kt:103)
at io.ktor.utils.io.ExceptionUtilsJvmKt$createConstructor$$inlined$safeCtor$1.invoke(ExceptionUtilsJvm.kt:90)
at io.ktor.utils.io.ExceptionUtilsJvmKt.tryCopyException(ExceptionUtilsJvm.kt:66)
at io.ktor.utils.io.ByteBufferChannelKt.rethrowClosed(ByteBufferChannel.kt:2404)
at io.ktor.utils.io.ByteBufferChannelKt.access$rethrowClosed(ByteBufferChannel.kt:1)
at io.ktor.utils.io.ByteBufferChannel.readRemaining$suspendImpl(ByteBufferChannel.kt:2063)
at io.ktor.utils.io.ByteBufferChannel.readRemaining(ByteBufferChannel.kt)
at io.ktor.utils.io.ByteReadChannel$DefaultImpls.readRemaining$default(ByteReadChannelJVM.kt:88)
at io.ktor.client.call.SavedCallKt.save(SavedCall.kt:73)
at io.ktor.client.statement.HttpStatement$execute$4.invokeSuspend(HttpStatement.kt:63)
at io.ktor.client.statement.HttpStatement$execute$4.invoke(HttpStatement.kt)
at io.ktor.client.statement.HttpStatement$execute$4.invoke(HttpStatement.kt)
at io.ktor.client.statement.HttpStatement.execute(HttpStatement.kt:50)
at io.ktor.client.statement.HttpStatement$execute$1.invokeSuspend(HttpStatement.kt)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:280)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at ClientKt.main(Client.kt:19)
at ClientKt.main(Client.kt)
Caused by: java.io.IOException: chunked transfer encoding, state: READING_LENGTH
at java.net.http/jdk.internal.net.http.common.Utils.wrapWithExtraDetail(Utils.java:330)
at java.net.http/jdk.internal.net.http.Http1Response$BodyReader.onReadError(Http1Response.java:758)
at java.net.http/jdk.internal.net.http.Http1AsyncReceiver.checkForErrors(Http1AsyncReceiver.java:297)
at java.net.http/jdk.internal.net.http.Http1AsyncReceiver.flush(Http1AsyncReceiver.java:263)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SynchronizedRestartableTask.run(SequentialScheduler.java:175)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:147)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:198)
at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Caused by: java.io.EOFException: EOF reached while reading
at java.net.http/jdk.internal.net.http.Http1AsyncReceiver$Http1TubeSubscriber.onComplete(Http1AsyncReceiver.java:591)
at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$ReadSubscription.signalCompletion(SocketTube.java:632)
at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$InternalReadSubscription.read(SocketTube.java:833)
at java.net.http/jdk.internal.net.http.SocketTube$SocketFlowTask.run(SocketTube.java:175)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:198)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:271)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:224)
at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$InternalReadSubscription.signalReadable(SocketTube.java:763)
at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$ReadEvent.signalEvent(SocketTube.java:941)
at java.net.http/jdk.internal.net.http.SocketTube$SocketFlowEvent.handle(SocketTube.java:245)
at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.handleEvent(HttpClientImpl.java:957)
at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.lambda$run$3(HttpClientImpl.java:912)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.run(HttpClientImpl.java:912)
HttpCookies: Encoding of request cookies is not preserved in CookiesStorage
When setting and sending a cookie that contains the "=" character while creating a request, the special characters are automatically encoded even if the encoding option is set to CookieEncoding.RAW.
@Test
fun test() = runTest {
val httpClient = HttpClient(CIO) {
install(HttpCookies)
}
val cookie = Cookie("test", "&%?#=$", encoding = CookieEncoding.RAW)
assertEquals("test=&%?#=$", renderCookieHeader(cookie)) // OK
val response = httpClient.get("https://example.com") {
header("Cookie", renderCookieHeader(cookie))
}
assertEquals("test=&%?#=$", response.request.headers.get("Cookie"))
}
It seems that the cause of the issue is that the cookies created in captureHeaderCookies within HttpCookies for storage do not have the encoding option specified.
internal suspend fun captureHeaderCookies(builder: HttpRequestBuilder) {
with(builder) {
val url = builder.url.clone().build()
val cookies = headers[HttpHeaders.Cookie]?.let { cookieHeader ->
LOGGER.trace("Saving cookie $cookieHeader for ${builder.url}")
// no "encoding" parameter
parseClientCookiesHeader(cookieHeader).map { (name, encodedValue) -> Cookie(name, encodedValue) }
}
cookies?.forEach { storage.addCookie(url, it) }
}
}
internal suspend fun sendCookiesWith(builder: HttpRequestBuilder) {
val cookies = get(builder.url.clone().build())
with(builder) {
if (cookies.isNotEmpty()) {
val cookieHeader = renderClientCookies(cookies)
// overwrite
headers[HttpHeaders.Cookie] = cookieHeader
...
}
}
}
BearerAuthProvider does not clear token if refreshTokens returns null
Expected:
For a client:
install(Auth) {
bearer {
loadTokens { ... }
refreshTokens { action() }
}
}
fun action(): BearerToken? { ... }
During a token refresh when action()
returns null
, the refresh action should be considered a failure and no more request should be performed.
Actual
When action()
returns null
the client will re-try the request with the same access token again.
Details
The return type of the refreshTokens
function is BearerTokens?
, implying that the function may return null
to indicate that the refresh has failed.
However, BearerAuthProvider
under the hood calls tokensHolder.setToken { refreshTokens(..) }
to update the cached token. Inside setToken
a null-check is made. If the result of refreshTokens()
is null, the token is not updated. The old token stays valid, but BearerAuthProvider
considers the refresh to be successful and re-tries the request.
This doesn't seem intended behavior, or at least it is not intuitive.
As I see it:
- If the caller really wants to re-use the same token they can explicitly return it from the
RefreshTokensParams
. - If
null
is not a valid response,refreshTokens
should not returnBearerTokens?
As a workaround, I wrapped the BearerAuthProvider
in a custom AuthProvider
that forwards everything directly to BearerAuthProvider
, except for refreshToken()
which checks for existing tokens and manually clears the cached token if needed.
override suspend fun refreshToken(response: HttpResponse): Boolean {
bearerAuthProvider.refreshToken(response)
val hasTokens = loadTokens() != null
if (!hasTokens) bearerAuthProvider.clearToken()
return hasTokens
}
HttpCache: InvalidCacheStateException thrown when Vary header has different entries is overly severe
Given a client does two API calls to the same endpoint.
The server returns first 200 - Vary: X-Requested-With,Accept-Encoding
and second 304 - Vary: X-Requested-With
The HttpCache crashes with
io.ktor.client.plugins.cache.InvalidCacheStateException: The entry for url: {URL} was removed from cache
- I don't think that this a good exception message
- I don't think that the cache should throw at all
According to the HTTP specs, it is "correct" that this is wrong in the first place.
The Vary header should be stable for the same endpoint.
See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Vary
However, throwing here as a client is probably not the best idea because "we clients" haven't done anything wrong. Further, we can't recover from this on our own - except of disabling caching (for everything or this specific endpoint). Which is more a band-aid isntead of a proper fix.
We (clients) have to inform "the backend" (could be even a 3-party one) to fix their server. Which is not always ideal or even possible.
I see the issue, that if the Vary
header changes, the HttpCache may not find the correct cache entry. Or doesn't even exist (as the exception message indicates).
But since we "should have" some cache available, I guess we can find the correct cache.
If I do understand the Vary
header correctly, it aims to avoid conditional API calls if the request didn't change. So a second API call shouldn't even be made at all. But this would also require to receive a proper `max-age` header. It seems this max-age is either already elapsed or is set to 0. In this case we have to do an API call anyways. So we might can simply ignore the Vary
header, right? Because max-age: 0
plus Vary
header doesn't make any sense (I guess, I'm not sure with that).
---
Reproducer:
class VaryTest {
private fun server() {
embeddedServer(Netty, port = 4445) {
routing {
get("/cache") {
if (call.request.headers.contains("200")) {
call.response.header("Vary", "X-Requested-With,Accept-Encoding")
call.respondText { "200 OK response" }
} else {
call.response.header("Vary", "X-Requested-With")
call.respond(HttpStatusCode.NotModified, null)
}
}
}
}.start(wait = false)
}
@Test
fun name() = runTest {
launch { server() }
delay(1000) // Fake delay to start the server
val storage = CacheStorage.Unlimited()
val client = httpClient().config {
install(HttpCache) {
publicStorage(storage)
}
}
client.get("http://localhost:4445/cache") {
header("200", "")
header("Accept-Encoding", "gzip,deflate")
}
println("First was successfully")
client.get("http://localhost:4445/cache") {
header("Accept-Encoding", "gzip,deflate")
}
println("We don't get here. Its crashing")
val cache = storage.findAll(Url("http://localhost:4444/cache"))
assert(cache.size == 1) { "Expected 1 but was ${cache.size}" }
}
}
Might be related:
The PR that introduces the exception can be found here:
Fix socket channel close handling
In the SocketBase class, we close the connected readJob's channel. This, however, can lead to threading issues if the channel is being written to or is closed in another thread. To resolve this, we should investigate where our code relies on this close operation and move the close to the coroutine which is writing to the channel. Also, instead of immediately closing the channel, we ought to wait for it to close naturally then cancel only if this times out.
Core
Allow suspend Ktor modules
Adding async functions inside Ktor modules currently requires a runBlocking
block, which creates a deadlock in the server creation. The linked issue ought to resolve the immediate problem, but this ticket should address this in a better way by allowing suspend functions to be used.
More overloads for StringValuesBuilder.appendAll
Add more overloads for appendAll
:
val builder = URLBuilder("https://github.com").apply {
// With Map
parameters.appendAll(mapOf("foo" to "bar", "baz" to "qux"))
parameters.appendAll(mapOf("test" to listOf("1", "2", "3")))
// With vararg Pair
parameters.appendAll("foo" to "bar", "baz" to "qux")
parameters.appendAll("test" to listOf("1", "2", "3"))
}
Url class mangles data URLs
Problem
Using ktor Url (io.ktor.http.Url
) for data Urls (https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs) mangles the Url. Specifically Ktor Url incorrectly changes part of the Url, transforming this:
data:image/svg+xml,...
to this, which can no longer be rendered properly:
data://localhost/image/svg+xml,...
Motivation
Our API vends Urls to images. Sometimes the images are references to image Urls, sometimes the images are embedded data Urls. Our API clients can happily render either of them. However, due to this Ktor bug the data Urls cannot be rendered.
Example
Given an SVG data Url:
data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M224%20387.814V512L32 320l192-192v126.912C447.375 260.152 437.794 103.016 380.93 0 521.287 151.707 491.48 394.785 224 387.814z'/%3E%3C/svg%3E
Which looks like this:
Using Ktor Url as follows:
fun main() {
val imageAsString =
"""data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M224%20387.814V512L32 320l192-192v126.912C447.375 260.152 437.794 103.016 380.93 0 521.287 151.707 491.48 394.785 224 387.814z'/%3E%3C/svg%3E"""
val imageAsKtorDataUrl = Url(imageAsString)
println(imageAsKtorDataUrl.toString())
}
Results in this when converted to String:
data://localhost/image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M224%20387.814V512L32 320l192-192v126.912C447.375 260.152 437.794 103.016 380.93 0 521.287 151.707 491.48 394.785 224 387.814z'/%3E%3C/svg%3E
Which no longer renders correctly:
Gradle Plugin
Basic compatibility with the KMP plugin
Almost all features of our Gradle plugin are designed to be used in JVM-only project. For example, applying of JavaApplication
plugin breaks compatibility with KMP 2.1.20+ (KTOR-8419)
At the basic level we should conditionally apply functionality depending on the plugins applied in a project:
- Don't apply
JavaApplication
plugin for KMP projects with KGP 2.1.20+ - Don't apply Shadow and JIB for KMP projects without JVM targets
- Properly add
ktor-bom
dependency to KMP projects
The plugin applies 'application' plugin, which is incompatible with KMP 2.1.20
KT-66542 makes Gradle Java plugins except java-base
one incompatible with KMP projects if the project is using Gradle version >8.6. As Ktor Gradle plugin auto-applies Gradle 'application' plugin – this produces a project configuration error.
Please don't apply the 'application' plugin if the KGP version in the project >=2.1.20 and instead use new JVM binaries DSL.
Infrastructure
Support Version Catalog
Ktor currently has many components, and it is complicated to manually define dependencies in lib.versions.toml
. Now many libraries use version-catalog
, which is simpler than BOM and can make work easier.
For example: KotlinCrypto/version-catalog
If implemented, it can be simply used:
// settings.gradle.kts
dependencyResolutionManagement {
versionCatalogs {
create("ktorLibs") {
from("io.ktor:ktor-version-catalog:3.0.3")
}
}
}
// <module>/build.gradle.kts
dependencies {
implementation(ktorLibs.client.core)
implementation(ktorLibs.client.cio)
}
Network
Support accessing resolved IP address on an instance of `io.ktor.network.sockets.InetSocketAddress`
Remmer says about Ktor documentation
Api symbol: io.ktor.network.sockets.InetSocketAddress
:
A function like
fun address() : ByteArray
would be useful for the class InetSocketAdress in some cases (of course after it is resolved)
Or maybe you can give me a hint how can I get the real address of a socket address
Server
Dependency injection Ktor extension
For this feature, we'll introduce Ktor's internal Inversion of Control (IoC) and Dependency Injection (DI) features.
The attached design ticket provides details for implementing.
This ticket will address the introduction of the feature along with base functionality. See Next Steps for aspects of the feature set that fall outside the scope.
Config deserialization does not respect `testApplication` environment
When overriding the configuration through, testApplication
it should be respected by the application.property
function.
Can easily be reproduced:
#application.yaml
config:
test: "test"
data class Config(val test: String)
fun Application.module() {
val config = property<Config>("config")
println(config.test)
}
testApplication {
environment {
config = ApplicationConfig("application.yaml")
.mergeWith(mapOf("config.test", "other"))
}
}
When running the test "other" should be printed but "test" was found.
Support suspendable module methods
suspend fun Application.internalApiModule() { }
2023-10-20 01:30:41.912 [main] INFO Application - Autoreload is disabled because the development mode is off.
Exception in thread "main" io.ktor.server.engine.internal.ReloadingException: Module function cannot be found for the fully qualified name 'com.shinami.ApplicationKt.internalApiModule'
at io.ktor.server.engine.internal.CallableUtilsKt.executeModuleFunction(CallableUtils.kt:59)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$launchModuleByName$1.invoke(ApplicationEngineEnvironmentReloading.kt:332)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$launchModuleByName$1.invoke(ApplicationEngineEnvironmentReloading.kt:331)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.avoidingDoubleStartupFor(ApplicationEngineEnvironmentReloading.kt:356)
at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.launchModuleByName(ApplicationEngineEnvironmentReloading.kt:331)
ApplicationConfig: Most of the content is absent after merging configs
I am loading the following configuration, but if we remove it, ApplicationConfig("application-test.yaml")
then it disappears. These contain duplicates keys, but the latter should override the former.
val s = ApplicationConfig("application.yaml")
// If we remove this line it works.
.mergeWith(ApplicationConfig("application-test.yaml"))
.mergeWith(dbContainer.getMapAppConfig())
println(s.toMap())
val app = s.property("app")
val config = app.getAs<AppConfig>()
Which prints:
{ktor={application.modules=[org.jetbrains.Application.module], deployment={port=8080, host=0.0.0.0}, development=true}, app={logging={level=INFO}, deployment={port=8080, host=0.0.0.0}, database={driverClassName=org.postgresql.Driver, host=localhost, port=60891, name=test, username=test, password=test, maxPoolSize=2, cachePrepStmts=true, prepStmtCacheSize=250, prepStmtCacheSqlLimit=2048}}}
Fields [logging, database, deployment] are required for type with serial name 'org.jetbrains.AppConfig', but they were missing
kotlinx.serialization.MissingFieldException: Fields [logging, database, deployment] are required for type with serial name 'org.jetbrains.AppConfig', but they were missing
at kotlinx.serialization.internal.PluginExceptionsKt.throwMissingFieldException(PluginExceptions.kt:20)
So we can see that before property("app")
thatApplicationConfig
contains the correct information.
But afterwards if we print s.property("app").getMap()
we can see that a lot of its content disappeared:
{database={driverClassName=org.postgresql.Driver, host=localhost, port=61072, name=test, username=test, password=test, maxPoolSize=2, cachePrepStmts=true, prepStmtCacheSize=250, prepStmtCacheSqlLimit=2048}}
Contents of the config files:
# Only used by EngineMain
ktor:
application.modules: [ org.jetbrains.Application.module ]
deployment:
port: 8080
host: 0.0.0.0
app:
logging:
level: INFO
# Only used by EmbeddedServer
deployment:
port: 8080
host: 0.0.0.0
test variant
ktor:
development: true
app:
database:
driverClassName: "$DB_DRIVER_CLASS_NAME:org.postgresql.Driver"
host: "$DB_HOST:localhost"
port: "$DATABASE_PORT:5432"
name: "$DB_NAME:ktor_sample"
username: "$DB_USERNAME:ktor_user"
password: "$DB_PASSWORD:ktor_password"
maxPoolSize: "$DB_MAX_POOL_SIZE:20"
cachePrepStmts: "$DB_CACHE_PREP_STMTS:true"
prepStmtCacheSize: "$DB_PREP_STMT_CACHE_SIZE:250"
prepStmtCacheSqlLimit: "$DB_PREP_STMT_CACHE_SQL_LIMIT:2048"
And the dynamic configuration coming from TestContainers.
fun getMapAppConfig(): ApplicationConfig =
MapApplicationConfig().apply {
put("app.database.host", host)
put("app.database.port", firstMappedPort.toString())
put("app.database.name", databaseName)
put("app.database.username", username)
put("app.database.password", password)
put("app.database.driverClassName", driverClassName)
put("app.database.maxPoolSize", "2")
put("app.database.cachePrepStmts", "true")
put("app.database.prepStmtCacheSize", "250")
put("app.database.prepStmtCacheSqlLimit", "2048")
}
Netty: NoSuchElementException or empty headers when responding with 204
To reproduce run plow (plow -c 1 http://localhost:8080/
) against the following server:
embeddedServer(Netty, port = 8080) {
install(
createApplicationPlugin("CallLogging") {
on(ResponseSent) { call ->
try {
val values = call.response.headers.allValues()
println("headers: $values")
} catch (e: Exception) {
println(e)
}
}
},
)
routing {
get("/") {
call.respond(HttpStatusCode.NoContent)
}
}
}.start(wait = true)
After few minutes, there will be a few unexpected java.util.NoSuchElementException
and empty headers in the ResponseHeaders
:
java.util.NoSuchElementException
at io.netty.handler.codec.DefaultHeaders$HeaderIterator.next(DefaultHeaders.java:1291)
at io.netty.handler.codec.DefaultHeaders$HeaderIterator.next(DefaultHeaders.java:1278)
at io.netty.handler.codec.HeadersUtils$StringEntryIterator.next(HeadersUtils.java:123)
at io.netty.handler.codec.HeadersUtils$StringEntryIterator.next(HeadersUtils.java:109)
at io.ktor.server.netty.http1.NettyHttp1ApplicationResponse$headers$1.getEngineHeaderNames(NettyHttp1ApplicationResponse.kt:129)
at io.ktor.server.response.ResponseHeaders.allValues(ResponseHeaders.kt:50)
at ServerKt.main$lambda$3$lambda$1$lambda$0(server.kt:63)
at io.ktor.server.application.hooks.ResponseSent$install$1.invokeSuspend(CommonHooks.kt:129)
at io.ktor.server.application.hooks.ResponseSent$install$1.invoke(CommonHooks.kt)
at io.ktor.server.application.hooks.ResponseSent$install$1.invoke(CommonHooks.kt)
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.server.engine.DefaultTransformKt$installDefaultTransformations$1.invokeSuspend(DefaultTransform.kt:31)
at io.ktor.server.engine.DefaultTransformKt$installDefaultTransformations$1.invoke(DefaultTransform.kt)
at io.ktor.server.engine.DefaultTransformKt$installDefaultTransformations$1.invoke(DefaultTransform.kt)
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)
A lot of requests have to be made to reproduce the problem. In one of the runs on my machine, the 297 requests out of 18354367 had no headers.
YAML configuration: NoSuchElementException when parameter is expanded with curly braces
To reproduce, execute the following code:
val config = ApplicationConfig("application.yaml")
val port = config.property("ktor.deployment.port").getString()
application.yaml:
ktor:
deployment:
port: ${PORT}
host: 0.0.0.0
As a result, an unexpected exception is thrown:
Exception in thread "main" java.util.NoSuchElementException: Char sequence is empty.
at kotlin.text.StringsKt___StringsKt.first(_Strings.kt:76)
at io.ktor.server.config.yaml.YamlConfigKt.resolveValue(YamlConfig.kt:181)
at io.ktor.server.config.yaml.YamlConfigKt.access$resolveValue(YamlConfig.kt:1)
at io.ktor.server.config.yaml.YamlConfig.checkEnvironmentVariables$check(YamlConfig.kt:140)
at io.ktor.server.config.yaml.YamlConfig.checkEnvironmentVariables$check(YamlConfig.kt:142)
at io.ktor.server.config.yaml.YamlConfig.checkEnvironmentVariables$check(YamlConfig.kt:142)
at io.ktor.server.config.yaml.YamlConfig.checkEnvironmentVariables$check(YamlConfig.kt:142)
at io.ktor.server.config.yaml.YamlConfig.checkEnvironmentVariables(YamlConfig.kt:147)
at io.ktor.server.config.yaml.YamlConfigJvmKt.configFromString(YamlConfigJvm.kt:42)
at io.ktor.server.config.yaml.YamlConfigJvmKt.YamlConfig(YamlConfigJvm.kt:28)
at io.ktor.server.config.yaml.YamlConfigLoader.load(YamlConfig.kt:27)
at io.ktor.server.config.ConfigLoader$Companion.load(ConfigLoaders.kt:40)
at io.ktor.server.config.HoconApplicationConfigKt.ApplicationConfig(HoconApplicationConfig.kt:106)
at SomeKt.main(some.kt:9)
at SomeKt.main(some.kt)
DI: Support for async dependencies
Often when building services, it's important to think about start-up time, and our options are often very limited.
I.e. let's say our server requires two "heavy" dependencies that take 10 seconds to initialise each. When working with DI frameworks, there is typically no support for async
, but this can elevate a lot of these pains. Normally in Ktor without DI I would do:
fun Application.module() {
val first = async { slowDependency() }
val two = async { otherSlowDependency() }
val service = async { MyService(first.await(), two.await()) }
routing {
get("/healthy") {
service.await()
call.respond("OK!")
}
}
}
Since Ktor supports "coroutines" out-of-the-box, and we offer a DI solution, I think we need to try and find a solution for this problem. This will allow our DI framework to scale with large DI graphs, which is often a problem, and when encountering this problem, there is often no clear solution and requires a lot of work to solve.
Excessive allocation of ApplicationConfig when loading multiple files from CLI
EngineMain
is used with -config=application.yaml -config=application-dev.yaml
cli arguments, and it results in 6 nested configs, and only 2 were expected.
DI lambda overloads
As a developer, I want to be able to declare dependencies like:
dependencies {
provide(::createLangChainFactory)
}
Where the function reference could have some arguments that are automatically resolved.
Dependency injection tooling for tests
Allow the use of overriding normal dependencies with replacement implementations when testing for added convenience.
DI validation occurs after "application started" message
This is quite misleading because the validation blocks the server from starting.
DI provide lambda type fails for java function returns
When using provide { someJavaFunction() }
the resulting reified type will be slightly different than the expected ktype, which causes misses during the dependency lookup.
DI type parameter with bounds not inferred
Here's a test to reproduce:
import io.ktor.server.plugins.di.*
import io.ktor.util.reflect.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlin.test.Test
interface Identifiable<ID> {
val id: ID
}
interface Repository<E: Identifiable<ID>, ID> : ReadOnlyRepository<E, ID>
interface ReadOnlyRepository<out E: Identifiable<ID>, ID>
interface User: Identifiable<Long> {
val name: String
}
data class FullUser(override val id: Long, override val name: String, val email: String): User
class ReflectTest {
@OptIn(ExperimentalSerializationApi::class)
@Test
fun test() {
val typeInfo = typeInfo<Repository<FullUser, Long>>()
val covariance = Supertypes * OutTypeArgumentsSupertypes
covariance.map(DependencyKey(typeInfo), 0)
.forEach { println(it) }
}
}
Routing: `accept` should return 406 if the `Accept` header isn't matched
HttpMultiAcceptRouteSelector
of Ktor server returns 400-Bad Request
, but should return 406 according to specs:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/406
Dependencies should be validated on startup
Due to lazy evaluation from delegate properties, missing dependency exceptions are happening after calling an endpoint. We ought to throw an exception during startup instead.
File configuration for dependencies
Introduce dependency factories to file configuration.
I'd like to be able to include some configuration like:
dependencies:
- io.ktor.chat.PostgresDatabaseKt.configureConnection
- io.ktor.chat.MessageRepository
Where I can reference application modules (functions), classes, or factories.
These will get loaded before all other modules and configure dependencies for them.
MicrometerMetrics: the `route` label can exceed length limit
When pushing metrics to Promethues, some metrics can be dropped as the route
label can be very long (mostly because of query parameters).
It would be nice if the plugin configuration can provide a property which allows these paths to be normalized trimmed or capped (the plugin will drop metrics with routes above a specific threshold), for example, by removing the query string of the requested URL.
Micrometer: Make route label configurable
Using Route.toString()
creates some problems with inconsistency and string length that make it unsuitable for reporting.
We ought to introduce an easy means to get the path of the route.
Here is some sample code for traversing the route selector to get this information:
private fun Route.toTelemetryAttributeValue(): String {
val parentRouteLabel = parent?.toTelemetryAttributeValue()
val selectorString = selector.toTelemetryAttributeValue()
return when {
parentRouteLabel == null -> selectorString
parentRouteLabel.endsWith('/') || selectorString.startsWith('/') -> "$parentRouteLabel$selectorString"
else -> "$parentRouteLabel/$selectorString"
}
}
private fun RouteSelector.toTelemetryAttributeValue(): String = when (this) {
is AuthenticationRouteSelector,
is RateLimitRouteSelector -> ""
is TrailingSlashRouteSelector -> "/"
else -> toString()
}
We'll probably want to use a similar mechanism but as an extension property so you can call Route.path
to get the representation.
Shared
Resources: Exclude a parent from query params when it is an object
@Resource("v1")
private data object V1 {
@Resource("api")
data class Api(val parent: V1)
}
@Test
fun testQueryParamsDoNotContainsObjectParent() {
assertEquals(resourcesFormat.encodeToQueryParameters(serializer = serializer<V1.Api>()), emptySet())
}
Expected :[]
Actual :[Parameter(name=parent, isOptional=false)]
Test Infrastructure
Add a way to create an `ApplicationCall` for testing
In Ktor 2.*
, an instance of the ApplicationCall
can be created with the following code:
val environment = createTestEnvironment { }
val application = Application(environment)
val call = TestApplicationCall(
application = application,
coroutineContext = coroutineContext
)
In Ktor 3.*
, a part of that API has been deleted, which makes it impossible to construct a TestApplicationCall
.
The TestApplicationCall
is handy for testing methods that accept an ApplicationCall
instance as a parameter.
https://kotlinlang.slack.com/archives/C0A974TJ9/p1729135899440159
Application instance access in testApplication
Currently, your only option is application {}
, but users may want to access attributes and the like, which can lead to some ugly code. We ought to include some means of accessing the application instance or some of its fields.
The TestApplication client should be configurable and mutable
Hi Ktor team!
This is a change request to make client in test code configurable.
A very common thing when testing Ktor routes is to configure the client to have ContentNegotiation (JSON, etc) or HttpCookies, etc. Such testing code is often extracted to common reusable function(s) to avoid error-prone repetitions in every test.
However currently Ktor 3.1.2 Official Testing docs suggest something quite bizarre:
@Test
fun testPostCustomer() = testApplication {
application {
main()
}
// This client is shadowing testApplication.client property!
val client = createClient {
install(ContentNegotiation) {
json()
}
}
}
https://ktor.io/docs/server-testing.html#configure-client
There are major issues with the official suggested Client Configuration in tests:
testApplication { }
already provides aclient
property, which is expected to be used but instead is being shadowed and IntelliJ 2025.1 is not even giving any warnings about the shadowing, not good. To suggest shadowing of official API is strange.- Secondly, no one wants to repeat the client configuration test after test for common things like ContentNegotiation and thus it often naturally leads to common test code being extracted into reusable functions, the problem with this is that the only good common interface to get the client is the
io.ktor.server.testing.ApplicationTestBuilder#client
, however it is an immutableval
and thus this leaves no good options for common code to programmed againstApplicationTestBuilder
with a custom client configuration.
There are two approaches one can attempt to make ApplicationTestBuilder#client
configurable:
Approach 1: add user abstraction on top of testApplication {}
class MyTestBuilder(val a: ApplicationTestBuilder) : TestApplicationBuilder(), ClientProvider {
override var client: HttpClient
get() = createClient {
install(HttpCookies) // etc custom configuration
}
set(value) {}
override fun createClient(block: HttpClientConfig<out HttpClientEngineConfig>.() -> Unit): HttpClient {
return a.createClient(block)
}
}
@KtorDsl
public fun testMyApplication(block: suspend MyTestBuilder.() -> Unit): TestResult {
return testApplication {
MyTestBuilder(this).block()
}
}
@Test
fun customClient() = testMyApplication {
val client1 = client
val client2 = client
assertNotSame(client1, client2)
}
This is very cumbersome and goes against official Ktor API which already provides testApplication {}
.
Approach 2: reflection
/**
* Overrides the default test client with the one provided in the [block].
* Ktor test framework doesn't let re-configuring the default client,
* instead a new one is created, and you have to manually reference it, which is ridiculous.
*/
fun ApplicationTestBuilder.overrideDefaultTestClient(block: HttpClientConfig<*>.() -> Unit) {
val applicationTestBuilder = this
val newClient = client.config(block)
newClient.toString()
applicationTestBuilder::class
.declaredMemberProperties
.single { it.name == "client" }
.let { it.javaField!! }
.apply {
isAccessible = true
set(applicationTestBuilder, lazyOf(newClient))
}
}
@Test
fun overrideDefaultTestClient() {
testApplication {
val client1 = client.hashCode()
overrideDefaultTestClient {
install(HttpCookies)
}
val client2 = client.hashCode()
assertNotSame(client1, client2)
}
}
This allows to use original Ktor API for testApplication {}
and achieves overriding the immutable client
property, however the approach is far from ideal.
Proposal
Allow configuring or overriding the ApplicationTestBuilder.client
, it is bizarre to expect real-world backend applications to not require a configured Test Client (ContentNegotiation, Cookies, etc, etc) and thus the argument is that testApplication { client }
should be configurable!
Something as simple as a setter method should be enough, similarly to what the Reflection approach achieves.
Other
Application job is not joined during shutdown
As a result, cancellation hooks are not executed. This can be problematic when coroutines launched from the application need cleanup.
See slack thread for reference: https://jetbrains.slack.com/archives/CUHBZQSCC/p1741351180930349
The job is cancelled in Application.dispose()
:
public fun dispose() {
applicationJob.cancel()
uninstallAllPlugins()
}
This ought to use cancelAndJoin()
instead.
Make Dependency Injection Usage Simple
At the moment, we have a lot of requests from users who don't understand how to use dependency injection frameworks with Ktor. Most of the dependency injection libraries use a repository pattern: you first have to register everything you have in the special repository instance. Compared to Spring Boot, which does not require this step.
Using Koin
- Init repository
val appModule = module {
// Define a singleton of Database
single<Database> { DatabaseImpl() }
// Define a factory of CustomerRepository
factory<CustomerRepository> { CustomerRepositoryImpl(get()) }
// Define a factory of CustomerService
factory<CustomerService> { CustomerServiceImpl(get()) }
// Define a factory of CustomerController
factory<CustomerController> { CustomerController(get()) }
}
- Install the plugin using repository
fun Application.main() {
// Install Koin
install(Koin) {
slf4jLogger()
modules(appModule)
}
}
- Inject
fun Application.main() {
// Inject CustomerController
val controller by inject<CustomerController>()
routing {
post("/api/v1/customer") {
val customer = call.receive<CustomerDto>()
val result = controller.addNewCustomer(customer)
call.respond(result)
}
}
}
Dependency Injection with Spring Boot
With Spring Boot, you don't have to enumerate all injectable classes in the repository instance. The classes marked by @Service
annotation will be automatically created when they are used in the constructor of @Controller
or @Autowired
parameter.
// A service interface that defines a method to print a message
interface MessageService {
fun printMessage()
}
// A service implementation that prints "Hello, world!" to the console
@Service
class MessageServiceImpl : MessageService {
override fun printMessage() {
println("Hello, world!")
}
}
// A controller class that depends on the message service
@Controller
class MessageController(
// The message service is injected through the constructor
val messageService: MessageService
) {
// A method that invokes the message service to print a message
fun showMessage() {
messageService.printMessage()
}
}
Goal
We need to find a way of removing the repository pattern (perfectly without using annotations) in Ktor. Reusing existing dependency injection frameworks to keep compatibility would also be great.
Obscure log message on server startup
Responding at http:io.ktor.server.engine.EngineConnectorConfigJvmKt$withPort$2@71aaf151
Dependency injection - handle delegate pattern
As a developer, I'd like to declare some base implementation for a given interface, access a wrapper type without hitting an ambiguous declaration exception.
Dependency injection named argument annotation
As a developer, I'd like to introduce qualifiers to my parameters when using DI.
For example:
class Foo(
@Named("test") val bar: Bar
)
This will map the parameter to the dependency key of the type Bar
with the named qualifier "test"
so that only Bar
instances with name "test"
are used.
Dependency injection - type parameter covariance supertypes
We ought to be able to handle type boundaries involving in / out keywords.
Also lambda parameters and return type covariance.
Optional dependencies and nullable type support
Support ignoring missing dependencies when the target is nullable, or if it includes a default.
This should work for parameter injection as well as calls like resolve<Foo?>()
.
Dependency keys for configuration items
As a developer, I'd like to be able to reference properties from my types to inject them via config.
For example, in the following function:
fun createDatabase(@Property("ktor.database.connectionUrl") connectionUrl: String)
We can populate the parameter with the corresponding property from our file configuration.
When referencing properties from the DSL, we can simply use the application environment to access the configuration items.
Dependency injection - reduce extension reliance for the sake of UX
There's a few places where we're using extension functions that hinder the UX with regards to auto-complete in the IDE.
Unix domain socket support at the Ktor Engine level
(This is a feature request--apologies if this is posted in the wrong location. I'm not able to un-mark it as a bug.)
This is similar to https://youtrack.jetbrains.com/issue/KTOR-781 , but would be to add uds feature parity to the CIO engine. This would be for more involved applications that use a unix domain socket that communicate using JSON.
Configuration access API improvements
The current way for accessing properties from the Application
context is quite cumbersome and could use some extension functions to reduce the boilerplate for basic access.
Here is an example of how to get a property:
environment.config.property(key).getString()
This could be improved with extension functions like:
inline fun <reified T> Application.property(key: String): T =
environment.config.property(key) // ...
inline fun <reified T> Application.propertyOrNull(key: String): T?
Dependency injection exceptions traceability
When running a server using the DI plugin, you'll naturally run into problems when you try to resolve a dependency that does not have a corresponding declaration. This is usually caught during the dependency tree validation step that occurs during startup.
There are a few shortcomings in the current implementation:
- The exception is thrown from the point of validation, and there is no indication of where we're actually trying to resolve it from.
- There is no good means for tracing what declarations have been provided to compare them against the exception.
- If you have multiple problems with resolving your dependencies, you'll have to resolve them one at a time because we throw immediately.
To fix:
- Include source information for where the dependency key is first referenced.
- Add trace details for when dependencies are declared, including any inferred keys through the covariance mapping.
- Build a summary of all problems when a problem is found during the validation step.
Dependency injection - cleanup support
As a Ktor developer, I want to "cleanup" dependencies when the application is stopped so that I do not expose myself to resource leaks.
Here is a possible syntax for the DSL:
dependencies {
// using scope for provide and cleanup
key<Closer> {
provide { first }
cleanup { it.closed = true }
}
// independent referencing for cleanup
provide<Closer>("second") { second }
cleanup<Closer>("second") { second.closed = true }
}
We can also include a general hook that defaults to closing Autoclosable
types.
install(DI) {
onShutdown = { key: DependencyKey ->
// cleanup by some global condition
}
}
Coroutines launched from RoutingContext are not cancelled upon server shutdown
- Add a cancellation hook for a coroutine launched from a routing endpoint
- Launch the server and hit the endpoint
- Stop the process normally
Expected: cancellation hook fires
Actual: cancellation hook does not fire
See thread: https://jetbrains.slack.com/archives/CUHBZQSCC/p1742392794916289
Configuration file deserialization
As a Ktor user, I would like to be able to read objects from my properties.
Ideally, we can introduce an interface like this:
fun <reified E> ApplicationEnvironment.property(key: String): E
To read in values from the configuration.
This should support all primitives and serializable classes.
For example, a configuration file like:
server:
port: 8080
domain: example.com
Should be accessible like environment.config.property<ServerDetails>("server")
where the data class is:
data class ServerDetails(
val port: Int,
val domain: String
)
Dependency injection via application module parameters
As a user, I want to simply introduce some parameters to my module that will be supplied by the application container.
For example:
fun Application.messageEndpoints(messages: Repository<Messages>) {
routing {
call.respond(messages.list())
}
}
This will populate messages
from the registry of dependencies using resolve
.
Add more common ContentType values
It would be good to cover e.g. https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types as a lot of common mime types are missing.
There's also a mix between all caps (CSS
, PNG
) and capitalized ( Xml
, Png
) so it would be useful to make these consistent.