Nicolas
06/23/2025, 10:54 AMfun Application.configureWebSocket(){
install(WebSockets) {
pingPeriod = 15.seconds
timeout = 15.seconds
maxFrameSize = kotlin.Long.MAX_VALUE
masking = false
}
}
routing {
webSocket("ws") {
val token = call.request.queryParameters["token"]
if (token == null) {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "No Token"))
return@webSocket
}
val decodedJWT = try { JwtFactory.buildverifier().verify(token) }
catch (e: Exception) {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Invalid Token: ${e.message}"))
return@webSocket
}
val userId: UUID = try { UUID.fromString(decodedJWT.getClaim(JwtClaimConstant.claimUserId).asString()) }
catch (e: Exception) {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Invalid Token: ${e.message}"))
return@webSocket
}
val sessionId = decodedJWT.id?.let {
runCatching { UUID.fromString(it) }.getOrNull()
} ?: run {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Invalid or missing sessionId (jti)"))
return@webSocket
}
<http://logger.info|logger.info>("$userId is connected")
try {
println("$userId start")
incoming.consumeEach {
when (it) {
is Frame.Text -> {
val text = it.readText()
println("tototot $userId Received: $text")
}
is Frame.Close -> {
println("tototot $userId WebSocket closed by server with reason: ${it.readReason()}")
}
is Frame.Ping -> {
println("tototot $userId ping: $it")
}
is Frame.Pong -> {
println("tototot $userId pong: $it")
} else -> {
println("tototot $userId else: $it")
}
}
}
} catch (e: Exception) {
println("$userId error $e")
} finally {
println("$userId finally remove")
}
println("$userId end")
}
}
and I found something weird If I open a websocket on my iOS phone, turnoff the internet (and close my phone) I keep logging this on KTOR:
io.ktor.websocket.WebSocket - WebSocket Pinger: received valid pong frame Frame PONG
For me if I close my phone the webscket should try a ping and should not receive a pong and close, For me the goal of ping pong is to avoid all this. I am not sure if I have doing everything good 🙂
I am hosting my server on Render
Thank you in advance for your timejeggy
06/24/2025, 8:53 AMval rawRouteAttribute = AttributeKey<String>("rawRouteAttribute")
environment.monitor.subscribe(Routing.RoutingCallStarted) { call ->
call.attributes.put(rawRouteAttribute, call.route.parent.toString())
}
and then retrieving it back in the CallLogging
plugin. But when looking at java flight recording, we can see that in our call logging plugin we get a few java.lang.IllegalStateException
exceptions and with the message of No instance for key AttributeKey: rawRouteAttribute
.
I'm wondering in what cases does ktor use the CallLogging
plugin but not fire a RoutingCallStarted
event?Poulastaa
06/24/2025, 8:14 PMLeo N
06/25/2025, 10:23 AMhallvard
06/26/2025, 8:06 AMryanbreaker
06/26/2025, 7:24 PMSamuel
06/27/2025, 3:48 AMreactormonk
06/27/2025, 8:31 AMbryanstern
06/27/2025, 10:42 PMhttpClient.wss(
request = {
url(websocketUrl)
header("foo", "bar")
},
) {
// TODO: consume websocket
}
solonovamax
06/27/2025, 10:47 PMLuv Kumar
07/01/2025, 1:50 PM383K
and with ktor my bundle size goes to around 617K
increasing bundle size by 234K
. I have found some old you track tickets around similar issues, but not able to find if there is any way around this size increase. Is this something which is expected or can it be improved ?ursus
07/01/2025, 10:42 PMSimon Binder
07/02/2025, 3:56 AMHttpStatement.execute
methods for this. But while this seems to work fine in an actual server (not written with ktor, I just want to test the client), this demo appears stuck, as if the response never arrives. Does anyone know what I need to do to test long-running stream responses?
package com.powersync
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.call.body
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.preparePost
import io.ktor.http.HttpHeaders
import io.ktor.http.content.OutgoingContent
import io.ktor.server.engine.ConnectorType
import io.ktor.server.engine.EngineConnectorBuilder
import io.ktor.server.response.header
import io.ktor.server.response.respond
import <http://io.ktor.server.routing.post|io.ktor.server.routing.post>
import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.ByteWriteChannel
import io.ktor.utils.io.awaitFreeSpace
import io.ktor.utils.io.readUTF8Line
import io.ktor.utils.io.writeStringUtf8
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
public suspend fun main() {
coroutineScope {
val server = testServer()
val client = server {
install(HttpTimeout)
install(ContentNegotiation)
}
launch {
val response = client.preparePost("<https://test.com/sync/stream>")
response.execute {
println("has response") // it never gets to this point
val channel: ByteReadChannel = it.body()
while (!channel.isClosedForRead) {
val line = channel.readUTF8Line()
if (line != null) {
println("has line: $line")
}
}
}
}
}
}
internal fun testServer(): (HttpClientConfig<*>.() -> Unit) -> HttpClient {
val application = ApplicationTestBuilder().apply {
engine {
connectors.add(EngineConnectorBuilder(ConnectorType.HTTPS).apply {
host = "<http://test.com|test.com>"
port = 443
})
}
routing {
post("/sync/stream") {
val content = object : OutgoingContent.WriteChannelContent() {
override suspend fun writeTo(channel: ByteWriteChannel) {
while (true) {
channel.awaitFreeSpace()
channel.writeStringUtf8("test\n")
channel.flush()
delay(1.0.seconds)
}
}
}
call.response.header(HttpHeaders.ContentType, "application/x-ndjson")
call.response.header(HttpHeaders.CacheControl, "no-store")
call.response.header(HttpHeaders.Connection, "keep-alive")
call.response.header("X-Accel-Buffering", "no")
call.respond(content)
}
}
}
return application::createClient
}
I can see in the debugger that the while
loop keeps running so I assume there's an internal buffer somewhere. Is there a way to turn that off?bk9735732777
07/03/2025, 6:33 AM# Stage 1: Cache Gradle dependencies
FROM gradle:latest AS cache
RUN mkdir -p /home/gradle/cache_home
ENV GRADLE_USER_HOME=/home/gradle/cache_home
COPY build.gradle.* gradle.properties /home/gradle/app/
COPY gradle /home/gradle/app/gradle
WORKDIR /home/gradle/app
RUN gradle clean build -i --stacktrace
# Stage 2: Build Application
FROM gradle:latest AS build
COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
# Build the fat JAR, Gradle also supports shadow
# and boot JAR by default.
RUN gradle buildFatJar --no-daemon
# Stage 3: Create the Runtime Image
FROM amazoncorretto:22 AS runtime
EXPOSE 8080
RUN mkdir /app
COPY --from=build /home/gradle/src/build/libs/*.jar /app/ktor-docker-sample.jar
ENTRYPOINT ["java","-jar","/app/ktor-docker-sample.jar"]
This is my docker fileAndrei Shilov
07/04/2025, 3:27 PMintercept
for Pipelines
between 2.x a and 3.2
we used to have nested routes like
route("x"){
get("") {...}
route("/b") {
intercept(ApplicationCallPipeline.Call) { ...}
get("")
}
}
in 3.2 nested intercept affects parent intercept and previously intercept(ApplicationCallPipeline.Call)
was always called before we end up in the handler code and now it is not called before the handler is called
Maybe we were relying on some undocumented behaviour ... do not really know.
Any help is appreciated, Thx in advance!Albertas
07/05/2025, 8:10 PMFields [apiKey, emails, templates] are required for type with serial name <config class>, but they were missing
The only thing that works is serializing primitive types.James
07/08/2025, 6:21 AMLukasz Kalnik
07/08/2025, 8:16 AMHttpClient
is declared in the shared multiplatform part. How can I inject OkHttp
with the custom DNS into the shared part only for Android?Aryan Gupta
07/08/2025, 4:22 PMAttributes
can be used to pass custom data throughout the request lifecycle. When using the OkHttpEngine
, I'm looking for a way to map these Ktor Attributes
to OkHttp Tags
.
As the current OkHttpEngine
implementation within KTOR doesn't seem to bridge this , how can I achieve this mapping?Arjan van Wieringen
07/09/2025, 12:58 PMHelio
07/10/2025, 7:51 AMktor.http.server.requests.upper_99
. But I need to add an extra property on top of this metric that says if the request took longer than 1s, add a new property saying that failed
otherwise met
.
Questions:
1. Is it possible to add new metric on top of this existing metric? Otherwise, would the code below be the most appropriate way to do it?
a. I know that after every endpoint execution Ktor logs the result, such as. 200 OK: GET - /healthcheck in 35ms
. Is it possible to extract the value instead of calculating it?
get("/healthcheck") {
val startTime = System.currentTimeMillis()
println(call.request.uri)
call.respondText(Constants.HEALTHCHECK_RESPONSE, ContentType.Text.Plain)
val endTime = System.currentTimeMillis() // Capture the end time
val duration = endTime - startTime // Calculate the duration
if (duration < 1000L) {
//Send Success metric
}
}
2. What would be the most appropriate way to get the name of the path. call.request.call.route.parent
? Is this the most accurate way? Assuming I don't want to get the value of the "queryParameter" and how the endpoint is defined instead.
Thanks for your help.Marcus Ilgner
07/10/2025, 10:10 AMAuthorization
information from the connection_init
message of a websocket connection for authentication? I have installed the Authentication
plugin - works fine for regular JWT Bearer Tokens in the Authorization
header of regular HTTP requests - and also set up a ContextFactory
for subscriptions can handle the authorization part.
Unfortunately, the authentication part doesn't work yet, as it seems like the connection_init
message isn't used by the Authentication
plugin to set up the principal. The client used is graphql-ws
and it looks like adding the auth information to connection_init
is the only supported method. I read this comment about how using regular HTTP headers for websockets might not be a good idea and think that probably there is some way to get Ktor to use the connection_init
payload. Any ideas / reading material?ayodele
07/10/2025, 11:59 AMursus
07/11/2025, 12:34 AMArjan van Wieringen
07/11/2025, 7:51 AMfun Application.myModule() {
val myDep: Dep by dependencies // or val myDep: Dep = dependencies.resolve();
// more code
}
I don't see why we should make make the functions so aware of a DI container. Also, this isn't really dependency injection. This is just a service container then 🙂 This is also pretty prevalent in the official KotlinConf backend app which runs on Koin.
Since modules are functions, and functions have arguments, isn't it better to do:
fun Application.myModule(myDep: Dep) { ... }
// And within Application scope
myModule(myDep = dependencies.resolve())
Is there a reason to do one over the other?dalexander
07/11/2025, 1:43 PMcookie()
method url encodes the cookie data for some reason which makes the data invalid, and trying to set the Cookie header directly seems to have no effect on the request based on what the logs are showing me (I don't see a Cookie header after setting it). Ktor version is 3.2.1Aryan Gupta
07/11/2025, 3:37 PMval cp = CertificatePinner.Builder().add("ktor.io", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=").build()
usePreconfiguredSession(
session = NSURLSession.sessionWithConfiguration(
NSURLSessionConfiguration.defaultSessionConfiguration,
NsUrlSessionDelegate(),
delegateQueue = NSOperationQueue()
),
delegate = KtorNSURLSessionDelegate(cp),
)
chrisjenx
07/11/2025, 3:41 PM//fun Application.installContextCaching() {
// intercept(ApplicationCallPipeline.Plugins) {
// withContextCache {
// proceedWith(subject)
// }
// }
//}
Seems the pipelining don't wrap interceptors which is what is normally expected.?Arjan van Wieringen
07/12/2025, 7:44 AMconfigure
function in the TestApplicationBuilder
docs.ursus
07/12/2025, 2:33 PMHttpClient(OkHttp) {
engine {
preconfigured = okHttpClient
}
install(DefaultRequest) {
url("<http://10.0.2.2:8081/>")
}
install(ContentNegotiation) {
json(json)
}
install(WebSockets)
}
val webSocketSession = ktorClient.webSocketSession("ws")
webSocketSession
doesnt seem to pickup the DefaultRequest
base url
If I duplicate the url again like this
val wsUrl = URLBuilder("<http://10.0.2.2:8081/>")
.apply {
protocol = URLProtocol.WS
path("ws")
}
.buildString()
val webSocketSession = ktorClient.webSocketSession(wsUrl)
then it works
Is that normal? Am I using it wrong?