While refactoring the filter chain used in one of our services, which is based on Kotlin
, Spring Boot
, WebFlux
, coRouter
& coroutines
, I've run in the following scenario:
* observationRegistry.asContextElement()
needs to be added early on to the context, so that the observation from http request is correctly propagated
* multiple separate concerns, such as adding trace baggage, logging incoming request etc., need to be implemented as individual filters
* since we are using coRouter
, some of the filters need to be only applied to some of the routes defined in the DSL
Here are the facilities I'm aware of / was able to find, which seem to be relevant for the problem at hand:
* CoWebFilter
* fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse)
in CoRouterFunctionDsl
* fun context(provider: suspend (ServerRequest) -> CoroutineContext)
in CoRouterFunctionDsl
Now, here are the problems I've run into:
* Building a chain of CoWebFilter
s would require making them all aware of which particular EPs to wrap and which to pass on
* Using fun filter(filterFunction)
from CoRouterFunctionDsl
allows to apply these in some parts of coRouter
, but these filter
functions aren't picking up the context that CoWebFilter
may have left in COROUTINE_CONTEXT_ATTRIBUTE
.
* Additionally, all filters created by fun filter(filterFunction)
will not inherit context from one another and aren't able to modify the context that the actual handler will use
* If fun context(provider)
is used, it's executed multiple times for 1 request. I.e. it will be called to create a context for each filter, and then for the corresponding handler.
In the end, I've ended up with the following "magical" implementation:
coRouter {
context { request ->
if (CoWebFilter.COROUTINE_CONTEXT_ATTRIBUTE !in request.attributes()) {
request.attributes()[CoWebFilter.COROUTINE_CONTEXT_ATTRIBUTE] =
Dispatchers.Unconfined + observationRegistry.asContextElement()
}
request.attributes()[CoWebFilter.COROUTINE_CONTEXT_ATTRIBUTE] as CoroutineContext
}
filter(baggageAddingFilter)
filter(requestLoggingFilter)
routes()
}
which is at least able to meet our current needs, but it still has a problem that filters added this way would only be able to modify coroutineContext
of one another by modifying request.attributes()[CoWebFilter.COROUTINE_CONTEXT_ATTRIBUTE]
explicitly.
IMO, Spring Framework could:
* provide more consistent support for persisting/inheriting coroutine context between parts of the execution chain.
* potentially look into adding a facility similar to fun context(provider)
of coRouter
that would be executed early on and provide the context for the first CoWebFilter in the chain
* reevaluate how many times
fun context(provider)should be executed by
coRouterduring handling of a single request (e.g. it could be for example used as a fallback to provide coroutineContext **once**, if by the time execution goes into
coRoutercode there was no
CoWebFilter` invoked).
Tested on: Spring Boot 3.2.4 / Spring 6.1.5
Comment From: sdeleuze
Hi, thanks for the detailed feedback and sorry for the delay, I think there is room for refinements indeed. I think we need to discuss to try to identify more focused individual refinements. Any chance you could provide focused repro(s) as a link to a repositiory or an attached project for the individual issues you are raising here?
I am wondering if adding a ServerRequest
extensions like request.coroutineContext()
could help combined with other context propagation refinements, any thoughts?
Building a chain of CoWebFilters would require making them all aware of which particular EPs to wrap and which to pass on
What do you mean by "EPs"?
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: ilya40umov
Alright, so as for the reproducer I have created something that you can refer to here:
* Router.kt - showing a couple of versions based on coRouter DSL and its filters
* V1 is a naive implementation and it does not work (as it's trying to rely on "withContext" propagation between the filters etc.)
* V2 is a working implementation that is based on CoWebFilter.COROUTINE_CONTEXT_ATTRIBUTE
attribute, but it's also super hacky
What do you mean by "EPs"?
Ah, sorry, EPs would stand for "endpoints" in this case.
Comment From: ilya40umov
Essentially, I would expect that V1 implementations would work out of the box: * MdcAddingFilterV1 * UserContextFilterV1
when defined in the router like this.
However, in reality to achieve context propagation between the filters I had to rewrite them like follows: * MdcAddingFilterV2 * UserContextFilterV2
And additionally set up a context provider on the coRouter level. And this context provider is basically called for each filter in the chain separately.
Comment From: sdeleuze
Thanks for your detailed feedback, I will let you know when I have clarified what we can/can't do and when.
Comment From: ilya40umov
Ran into another (mostly unrelated) problem with the coroutine context propagation and raised the following PR: https://github.com/spring-projects/spring-framework/pull/33548