Please provide a way to bind objects to the request and acces them from anywhere
In Ktor, accessing the incoming call is easy: just use the call field. However, this field is not global, it is provided by the REST API builder.
Assume you are building a microservice with Ktor. You have your REST endpoints, a service layer, and some repositories - a typical 3-level enterprise stack. You do authentication via JWT. All of that works nicely with Ktor.
Now we would like to call another microservice from one of our services. To do so, we require the JWT token which was passed to the call. We're in the middle of a service-layer method, probably 5 or 6 function calls deep into the JVM call stack. The call object (which contains the token in its header) is way out of reach. Passing it down as an argument through all the functions in the service layer would be a major pain as well.
Coming from the Spring framework, for a case like this, you have the SecurityContext. You would call the static method SecurityContextHolder.getContext() and get the exact instance of the context which is associated with your current call. This is accomplished via a ThreadLocal<SecurityContext> that is hidden inside the SecurityContextHolder. When a call is received, Spring generates the security context, and clears it when call processing is done.
Other prominent use cases for request-attached data are:
- Sessions maintained by O/R-mappers (e.g. JPA)
- Database transactions
- Access control decisions
As far as I know, Ktor at the moment provides no such facilities. Mimicking the spring approach directly is also not feasible, as coroutines and ThreadLocal in general do not play nice with each other.
I've found this article which outlines an approach for storing such data in the CoroutineContext. However, this has some issues as well:
- Coroutine contexts are, to my knowledge, not nested by default. So whenever I start a sub-coroutine (e.g. via
async), the context of the parent coroutine (the one which invokedasync()) is not visible to the child (the one which was produced byasync()). This is in general very unfortunate, because the programmer A) needs to be aware that the context contains data which might be needed downstream and B) needs to pass the data along explicitly in the coroutine generator function. - The coroutine context API is very convoluted. I just tried to follow the article, the developer experience is honestly quite bad (in essence it's just a hash map, with type safety factored in). It feels like I have to write a lot of code to accomplish something that is simple, and that is also an often-needed feature. The upshot is: ideally I shouldn't have to touch it as a Ktor user.
The Ktor-side API which I would imagine is something like this:
object CallContext {
suspend fun get(key: String): Any { ... }
suspend fun set(key: String, value: Any): Any { ... }
}
... and ideally the programmer should not have to explicitly pass along coroutine contexts. The functions are suspending to force the caller to use them from within a coroutine (this allows the implementation to make use of the coroutineContext). I'm not super experienced with coroutines (yet), so maybe there is a way to do it like this even today. Maybe Ktor already has something like this?
Please feel free to correct me if I took a wrong turn somewhere. I'm still quite new to all of this.
I'm also very new to Kotlin, so there may be something I'm missing here, but is there any update on this?
I didn't have JWTs in mind when I was mucking about with this sort of thing earlier, rather I was thinking about request tracing and APM - it would be great to have access to some sort of context object in order to reference current spans etc and send the data over to APM without having to pass the context all the way down the stack.
https://github.com/Kotlin/kotlinx.coroutines/blob/master/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt seemed to provide a hint in the direction to go in, but as far as I can see wouldn't allow for new contexts (for instance a new span) when launching a coroutine deeper in the call stack (for instance performance tracing of database interactions or network calls from within client libraries).
I was hoping to create a library to plug into my existing APM solution that could be relatively transparent (in terms of tracing implementation) to the consuming service, but passing a context explicitly down the whole stack prevents this.
.net has a great concept of AsyncLocals - does/will Kotlin have something similar for coroutines? They would be perfect for this sort of thing.
@LyleDavis AsyncLocal sounds like what is needed here. I think what we're supposed to use is the coroutine context. However, back in the day when I attempted to do this (as you can see, the ticket is quite old by now) I found myself losing the context frequently by accident / by not explicitly passing it onto child coroutines. I'm not sure why it was designed this way, but this is one of the many straws that ultimately broke the camel's back and forced me to go back to Spring.
Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.
This is indeed very unfortunate. Are there any updates on this?
The ApplicationCall itself already have typed attributes (it actually always had them). So the only you need is to pass an instance of it everywhere.
You can use the coroutine context elements to pass application call to everywhere + ThreadLocalElement. Doing this you most likely have this magic failed in many corner cases due to missing or overlapping calls so happy debugging this. This is why we don't provide this magic out of the box.
Spring had the SecurityContextHolder for a long time, which is globally acessible in an (internally managed) ThreadLocal. They're doing just fine with it, never had any complaints about it. The real issue is that (to my knowledge) there's no such thing as a CoroutineLocal. And that's painful. I absolutely won't be handing down security contexts through dozens of nested methods (which do nothing with it except passing it onwards) as parameters just because I need it far down the line. That's just not feasible in large applications. It's not just security, what's next? You'll soon find yourself passing contexts all over the place.
Spring had the
SecurityContextHolderfor a long time, which is globally acessible in an (internally managed)ThreadLocal. They're doing just fine with it, never had any complaints about it. The real issue is that (to my knowledge) there's no such thing as aCoroutineLocal.
Actually, there is the context. But the problem is that it is only available in suspend functions. So it should be coroutine context + thread locals with ThreadLocalElement support that could do the trick.
We are at vacation here so I don't have a dev environment so can't verify. But the IDEA is like the following (JVM only):
class MyContext
private val threadLocal = ThreadLocal<MyContext>(null)
suspend fun withMyContext(block: suspend () -> Unit) {
val myContext = MyContext()
threadLocal.set(myContext)
val ctx = coroutineContext + ThreadLocalElement(threadLocal)
withContext(ctx) {
block()
}
}
val currentContext: MyContext
get() = threadLocal.get() ?: error("No current context")
- add a global interceptor to wrap every call into withMyContext
@cy6erGn0m could you please elaborate more on how to actually register such a global interceptor?
@cy6erGn0m could you please elaborate more on how to actually register such a global interceptor?
It is possible to intercept the application pipeline on early enough stage, like application.intercept(Call) {}
The other approach is described here
https://ktor.io/docs/custom-plugins.html
In case anyone lands into this thread, this tiny snippet seems to work to make the call available downstream, you can easily adapt it to only expose the auth information.
import io.ktor.server.application.*
import kotlinx.coroutines.asContextElement
import kotlinx.coroutines.withContext
private val callThreadLocal = ThreadLocal<ApplicationCall>()
object CallContextHolder {
fun get(): ApplicationCall {
return callThreadLocal.get()
?: throw AssertionError("No ApplicationCall in context, is CallContextPlugin installed?")
}
}
val CallContextPlugin = createApplicationPlugin(name = "CallContextPlugin") {
application.intercept(ApplicationCallPipeline.ApplicationPhase.Call) {
withContext(callThreadLocal.asContextElement(call)) {
proceed()
}
}
}
Be aware that this solution makes it harder to test, you will need to provide utility methods as part of CallContextHolder to set mock data if needed (similar to Spring SecurityContextHolder)