-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
KTOR-7194 Deferred session fetching for public endpoints (#4609)
* KTOR-7194 Deferred session fetching for public endpoints * KTOR-7194 Use system property for deferred sessions * fixup! KTOR-7194 Use system property for deferred sessions * Reduce test size due to timeouts on native agent
- Loading branch information
Showing
12 changed files
with
288 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
...or-server-plugins/ktor-server-auth/jvm/test/io/ktor/tests/auth/SessionAuthDeferredTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/* | ||
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package io.ktor.tests.auth | ||
|
||
import io.ktor.client.request.* | ||
import io.ktor.http.* | ||
import io.ktor.server.application.* | ||
import io.ktor.server.auth.* | ||
import io.ktor.server.response.* | ||
import io.ktor.server.routing.* | ||
import io.ktor.server.sessions.* | ||
import io.ktor.server.sessions.serialization.* | ||
import io.ktor.server.testing.* | ||
import kotlinx.serialization.Serializable | ||
import kotlinx.serialization.json.Json | ||
import kotlin.test.* | ||
|
||
class SessionAuthDeferredTest : SessionAuthTest() { | ||
|
||
@BeforeTest | ||
fun setProperty() { | ||
System.setProperty("io.ktor.server.sessions.deferred", "true") | ||
} | ||
|
||
@AfterTest | ||
fun clearProperty() { | ||
System.clearProperty("io.ktor.server.sessions.deferred") | ||
} | ||
|
||
@Test | ||
fun sessionIgnoredForNonPublicEndpoints() = testApplication { | ||
val brokenStorage = object : SessionStorage { | ||
override suspend fun write(id: String, value: String) = Unit | ||
override suspend fun invalidate(id: String) = error("invalidate called") | ||
override suspend fun read(id: String): String = error("read called") | ||
} | ||
application { | ||
install(Sessions) { | ||
cookie<MySession>("S", storage = brokenStorage) { | ||
serializer = KotlinxSessionSerializer(Json.Default) | ||
} | ||
} | ||
install(Authentication.Companion) { | ||
session<MySession> { | ||
validate { it } | ||
} | ||
} | ||
routing { | ||
authenticate { | ||
get("/authenticated") { | ||
call.respondText("Secret info") | ||
} | ||
} | ||
post("/session") { | ||
call.sessions.set(MySession(1)) | ||
call.respondText("OK") | ||
} | ||
get("/public") { | ||
call.respondText("Public info") | ||
} | ||
} | ||
} | ||
val withCookie: HttpRequestBuilder.() -> Unit = { | ||
header("Cookie", "S=${defaultSessionSerializer<MySession>().serialize(MySession(1))}") | ||
} | ||
|
||
assertEquals(HttpStatusCode.Companion.OK, client.post("/session").status) | ||
assertEquals(HttpStatusCode.Companion.OK, client.get("/public", withCookie).status) | ||
assertFailsWith<IllegalStateException> { | ||
client.get("/authenticated", withCookie).status | ||
} | ||
} | ||
|
||
@Serializable | ||
data class MySession(val id: Int) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
...server-plugins/ktor-server-sessions/common/src/io/ktor/server/sessions/SessionDeferral.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/* | ||
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package io.ktor.server.sessions | ||
|
||
import io.ktor.server.application.ApplicationCall | ||
|
||
/** | ||
* System property flag to defer session data retrieval to the point of access rather than at the start of the | ||
* call lifecycle. | ||
*/ | ||
internal const val SESSIONS_DEFERRED_FLAG = "io.ktor.server.sessions.deferred" | ||
|
||
internal expect fun isDeferredSessionsEnabled(): Boolean | ||
|
||
/** | ||
* Creates a lazy loading session from the given providers. | ||
*/ | ||
internal expect fun createDeferredSession(call: ApplicationCall, providers: List<SessionProvider<*>>): StatefulSession |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
...r-sessions/jsAndWasmShared/src/io/ktor/server/sessions/SessionDeferral.jsAndWasmShared.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/* | ||
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package io.ktor.server.sessions | ||
|
||
import io.ktor.server.application.ApplicationCall | ||
|
||
internal actual fun isDeferredSessionsEnabled(): Boolean = false | ||
|
||
internal actual fun createDeferredSession(call: ApplicationCall, providers: List<SessionProvider<*>>): StatefulSession = | ||
TODO("Deferred session retrieval is currently only available for JVM") |
8 changes: 8 additions & 0 deletions
8
...erver-plugins/ktor-server-sessions/jvm/src/io/ktor/server/sessions/SessionDeferral.jvm.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/* | ||
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package io.ktor.server.sessions | ||
|
||
internal actual fun isDeferredSessionsEnabled(): Boolean = | ||
System.getProperty(SESSIONS_DEFERRED_FLAG)?.toBoolean() == true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
...or-server-sessions/jvmAndPosix/src/io/ktor/server/sessions/BlockingDeferredSessionData.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package io.ktor.server.sessions | ||
|
||
import io.ktor.server.application.ApplicationCall | ||
import kotlinx.coroutines.Deferred | ||
import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
import kotlinx.coroutines.runBlocking | ||
import kotlin.coroutines.CoroutineContext | ||
import kotlin.reflect.KClass | ||
|
||
/** | ||
* An implementation of [StatefulSession] that lazily references session providers to | ||
* avoid unnecessary calls to session storage. | ||
* All access to the deferred providers is done through blocking calls. | ||
*/ | ||
internal class BlockingDeferredSessionData( | ||
val callContext: CoroutineContext, | ||
val providerData: Map<String, Deferred<SessionProviderData<*>>>, | ||
) : StatefulSession { | ||
|
||
private var committed = false | ||
|
||
@OptIn(ExperimentalCoroutinesApi::class) | ||
override suspend fun sendSessionData(call: ApplicationCall, onEach: (String) -> Unit) { | ||
for (deferredProvider in providerData.values) { | ||
// skip non-completed providers because they were not modified | ||
if (!deferredProvider.isCompleted) continue | ||
val data = deferredProvider.getCompleted() | ||
onEach(data.provider.name) | ||
data.sendSessionData(call) | ||
} | ||
committed = true | ||
} | ||
|
||
override fun findName(type: KClass<*>): String { | ||
val entry = providerData.values.map { | ||
it.awaitBlocking() | ||
}.firstOrNull { | ||
it.provider.type == type | ||
} ?: throw IllegalArgumentException("Session data for type `$type` was not registered") | ||
|
||
return entry.provider.name | ||
} | ||
|
||
override fun set(name: String, value: Any?) { | ||
if (committed) { | ||
throw TooLateSessionSetException() | ||
} | ||
val providerData = | ||
providerData[name] ?: throw IllegalStateException("Session data for `$name` was not registered") | ||
setTyped(providerData.awaitBlocking(), value) | ||
} | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
private fun <S : Any> setTyped(data: SessionProviderData<S>, value: Any?) { | ||
if (value != null) { | ||
data.provider.tracker.validate(value as S) | ||
} | ||
data.newValue = value as S | ||
} | ||
|
||
override fun get(name: String): Any? { | ||
val providerDataDeferred = | ||
providerData[name] ?: throw IllegalStateException("Session data for `$name` was not registered") | ||
val providerData = providerDataDeferred.awaitBlocking() | ||
return providerData.newValue ?: providerData.oldValue | ||
} | ||
|
||
override fun clear(name: String) { | ||
val providerDataDeferred = | ||
providerData[name] ?: throw IllegalStateException("Session data for `$name` was not registered") | ||
val providerData = providerDataDeferred.awaitBlocking() | ||
providerData.oldValue = null | ||
providerData.newValue = null | ||
} | ||
|
||
private fun Deferred<SessionProviderData<*>>.awaitBlocking() = | ||
runBlocking(callContext) { await() } | ||
} |
20 changes: 20 additions & 0 deletions
20
...or-server-sessions/jvmAndPosix/src/io/ktor/server/sessions/SessionDeferral.jvmAndPosix.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/* | ||
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package io.ktor.server.sessions | ||
|
||
import io.ktor.server.application.ApplicationCall | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.CoroutineStart | ||
import kotlinx.coroutines.async | ||
|
||
internal actual fun createDeferredSession(call: ApplicationCall, providers: List<SessionProvider<*>>): StatefulSession = | ||
BlockingDeferredSessionData( | ||
call.coroutineContext, | ||
providers.associateBy({ it.name }) { | ||
CoroutineScope(call.coroutineContext).async(start = CoroutineStart.LAZY) { | ||
it.receiveSessionData(call) | ||
} | ||
} | ||
) |
Oops, something went wrong.