How to pass context back into the WebFilter when using SpringBootWebFlux with coroutines - spring

Suppose I have the following application with a WebFilter and I'm trying to pass some context back from the controller into the filter.
#RestController
class MyController {
#PostMapping("/test")
suspend fun postSomething(): ResponseEntity<Unit> {
val valueFromFilter = coroutineContext[ReactorContext.Key]?.context?.get<String>("myKey") ?: "EMPTY"
logger.info { "Inside handler = $valueFromFilter" } // this works since Reactor populate coroutineContext with respective ReactorContext
coroutineContext[ReactorContext.Key]?.context?.put("handlerKey", "hello")
return ResponseEntity.ok().build()
}
}
#Component
class MyFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
logger.info { "Before" }
return chain.filter(exchange)
.contextWrite(Context.of("myKey", "myValue"))
.doOnEach {
val valueFromHandler = if (it.contextView.hasKey("handlerKey")) it.contextView.get<String>("handlerKey") else "EMPTY"
logger.info { "After handler = $valueFromHandler" } // But this doesn't work since Reactor doesn't restore ReactorContext from respective coroutineContext
}
}
}
#SpringBootApplication
class MyApplication
fun main(args: Array<String>) {
run(MyApplication::class.java, *args)
}
In this example if we made a request to /test it would print the following
Before
Inside handler = myValue
After handler = EMPTY
I understand that when we call chain.filter(exchange) to proceed with the request and the handler is a Kotlin suspend function the framework fills in coroutineContext with ReactorContext.Key to actualReactorContext. Although my question is why doesn't Spring restore the context that I may have filled in the controller back, so I can use it after calling chain.filter(exchange). Moreover if there is anyway possible to do this currently.

Just figured out I was looking into the wrong way of doing it. Instead one can simply just use the attributes of ServerWebExchange to pass context around like
#RestController
class MyController {
#PostMapping("/test")
suspend fun postSomething(webExchange: ServerWebExchange): ResponseEntity<Unit> {
val valueFromFilter = webExchange.attributes["myKey"] as? String ?: "EMPTY"
logger.info { "Inside handler = $valueFromFilter" }
webExchange.attributes["handlerKey"] = "hello"
return ResponseEntity.ok().build()
}
}
#Component
class MyFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
logger.info { "Before" }
exchange.attributes["myKey"] = "myValue"
return chain.filter(exchange)
.doOnEach {
val valueFromHandler = exchange.attributes["handlerKey"] as? String ?: "EMPTY"
logger.info { "After handler = $valueFromHandler" }
}
}
}
This way we can pass context between both the filter and the handler.

Related

Spring WebClient Error - reactor.core.publisher.FluxOnAssembly cannot be cast to class reactor.core.publisher.Mono

Following is the rest service that I have implemented
#GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE, value = "/events")
Flux<Event> events() {
Flux<Event> eventFlux = Flux.fromStream(Stream.generate {new Event(System.currentTimeMillis(), LocalDate.now())})
Flux<Long> durationFlux = Flux.interval(Duration.ofSeconds(1))
return Flux.zip(eventFlux, durationFlux)
.map {it.t1}
}
public static void main(String[] args) {
SpringApplication.run(ReactiveServiceApplication)
}
The webclient to consume this looks as follows.
#Bean
WebClient client() {
return WebClient.create("http://localhost:8080")
}
#Bean
CommandLineRunner demo (WebClient client) {
return { args ->
client.get().uri("/events")
.accept(MediaType.TEXT_EVENT_STREAM)
.exchange()
.flatMap { response -> response.bodyToFlux(Event.class) }
.subscribe { println it }
}
}
public static void main(String[] args) {
new SpringApplicationBuilder(ReactiveClientApplication)
.properties(Collections.singletonMap("server.port","8081"))
.run(args)
}
The error I am getting is as follows.
reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.ClassCastException: class reactor.core.publisher.FluxOnAssembly cannot be cast to class reactor.core.publisher.Mono (reactor.core.publisher.FluxOnAssembly and reactor.core.publisher.Mono are in unnamed module of loader 'app')
Caused by: java.lang.ClassCastException: class reactor.core.publisher.FluxOnAssembly cannot be cast to class reactor.core.publisher.Mono (reactor.core.publisher.FluxOnAssembly and reactor.core.publisher.Mono are in unnamed module of loader 'app')
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118) ~[reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
Why is the error happening ?
The reason was that using flatMap actualy returns a Mono. So the closure { response -> response.bodyToFlux(Event.class) } was actually returning a Flux where as the flatMap was expecting a Mono.
Changing to flatMapMany will fix this. Code below.
CommandLineRunner getFluxDemoExchange (WebClient client) {
return { args ->
client.get().uri("/events")
.accept(MediaType.TEXT_EVENT_STREAM)
.exchange()
.flatMapMany { response -> response.bodyToFlux(Event.class) }
.subscribe { println it }
}
}

Spring WebFlux Route always returns 404

I am working on a simple project which uses Spring Boot 2 with Spring WebFlux using Kotlin.
I wrote test for my handler function (in which I mock the dependencies using Mockito).
However, it seems like my route function does not trigger the handler, as all of my requests return HTTP 404 NOT FOUND (even though the route is correct).
I have looked at various other projects to find out what how these tests are supposed to be written (here, here), but the problem persists.
The code is as follows (and can also be found on GitHub):
UserRouterTest
#ExtendWith(SpringExtension::class, MockitoExtension::class)
#Import(UserHandler::class)
#WebFluxTest
class UserRouterTest {
#MockBean
private lateinit var userService: UserService
#Autowired
private lateinit var userHandler: UserHandler
#Test
fun givenExistingCustomer_whenGetCustomerByID_thenCustomerFound() {
val expectedCustomer = User("test", "test")
val id = expectedCustomer.userID
`when`(userService.getUserByID(id)).thenReturn(Optional.ofNullable(expectedCustomer))
val router = UserRouter().userRoutes(userHandler)
val client = WebTestClient.bindToRouterFunction(router).build()
client.get()
.uri("/users/$id")
.accept(MediaType.ALL)
.exchange()
.expectStatus().isOk
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(User::class.java)
}
}
User
#Entity
class User(var username : String, var password: String) {
#Id
val userID = UUID.randomUUID()
}
UserRepository
#Repository
interface UserRepository : JpaRepository<User, UUID>{
}
UserService
#Service
class UserService(
private val userRepository: UserRepository
) {
fun getUserByID(id: UUID): Optional<User> {
return Optional.of(
try {
userRepository.getOne(id)
} catch (e: EntityNotFoundException) {
User("test", "test")
}
)
}
fun addUser(user: User) {
userRepository.save(user)
}
}
UserHandler
#Component
class UserHandler(
private val userService: UserService
) {
fun getUserWithID(request: ServerRequest): Mono<ServerResponse> {
val id = try {
UUID.fromString(request.pathVariable("userID"))
} catch (e: IllegalArgumentException) {
return ServerResponse.badRequest().syncBody("Invalid user id")
}
val user = userService.getUserByID(id).get()
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(user))
}
}
UserRouter
#Configuration
class UserRouter {
#Bean
fun userRoutes(userHandler: UserHandler) = router {
contentType(MediaType.APPLICATION_JSON_UTF8).nest {
GET("/users/{userID}", userHandler::getUserWithID)
GET("") { ServerResponse.ok().build() }
}
}
}
EDIT
To route based on the presence of one or more query parameter (regardless of their values), we can do the following:
UserRouter
#Configuration
class UserRouter {
#Bean
fun userRoutes(userHandler: UserHandler) = router {
GET("/users/{userID}", userHandler::getUserWithID)
(GET("/users/")
and queryParam("username") { true }
and queryParam("password") { true }
)
.invoke(userHandler::getUsers)
}
}
Note that GET("/users/?username={username}", userHandler::getUsersWithUsername) does not work.
The way the router is configured - contentType(MediaType.APPLICATION_JSON_UTF8).nest - will only match requests that have this content type, so you would have to either remove the contentType prerequisite or change the test to include it
client.get()
.uri("/users/$id")
.accept(MediaType.ALL)
.header("Content-Type", "application/json;charset=UTF-8")
.exchange()
.expectStatus().isOk
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(User::class.java)

Using WebClient to propagate request headers received in a Spring Webflux applications to downstream services

I have two kinds of Webflux applications, annotation-based and route-based. These applications are called with a set of headers, some of which (Open Tracing) I need to propagate in downstream calls using WebClient.
If these were normal Spring WebMvc applications I would use a Filter to keep the selected headers in a ThreadLocal, access it in a RestTemplate interceptor to send them to subsequent services and clear the ThreadLocal.
What's the proper way to replicate this behaviour in WebFlux applications?
I solved it using Project Reactor's Context to store the headers in a WebFilter. Then they are gotten in the WebClient's ExchangeFilterFunction. Here's the whole solution:
WebFilter
class OpenTracingFilter(private val openTracingHeaders: Set<String>) : WebFilter {
private val logger = LoggerFactory.getLogger(javaClass)
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return chain.filter(exchange)
.subscriberContext { ctx ->
var updatedContext = ctx
exchange.request.headers.forEach {
if (openTracingHeaders.contains(it.key.toLowerCase())) {
logger.debug("Found OpenTracing Header - key {} - value {}", it.key, it.value[0])
updatedContext = updatedContext.put(it.key, it.value[0])
}
}
updatedContext
}
}
}
OpenTracingExchangeFilterFunction
class OpenTracingExchangeFilterFunction(private val headers: Set<String>) : ExchangeFilterFunction {
private val logger = LoggerFactory.getLogger(javaClass)
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
logger.debug("OpenTracingExchangeFilterFunction - filter()")
return OpenTracingClientResponseMono(request, next, headers)
}
}
OpenTracingClientResponseMono
class OpenTracingClientResponseMono(private val request: ClientRequest,
private val next: ExchangeFunction,
private val headersToPropagate: Set<String>) : Mono<ClientResponse>() {
private val logger = LoggerFactory.getLogger(javaClass)
override fun subscribe(subscriber: CoreSubscriber<in ClientResponse>) {
val context = subscriber.currentContext()
val requestBuilder = ClientRequest.from(request)
requestBuilder.headers { httpHeaders ->
headersToPropagate.forEach {
if(context.hasKey(it)) {
logger.debug("Propagating header key {} - value{}", it, context.get<String>(it))
httpHeaders[it] = context.get<String>(it)
}
}
}
val mutatedRequest = requestBuilder.build()
next.exchange(mutatedRequest).subscribe(subscriber)
}
}
OpenTracingConfiguration
#Configuration
class OpenTracingConfiguration(private val openTracingConfigurationProperties: OpenTracingConfigurationProperties) {
#Bean
fun webClient(): WebClient {
return WebClient.builder().filter(openTracingExchangeFilterFunction()).build()
}
#Bean
fun openTracingFilter(): WebFilter {
return OpenTracingFilter(openTracingConfigurationProperties.headers)
}
#Bean
fun openTracingExchangeFilterFunction(): OpenTracingExchangeFilterFunction {
return OpenTracingExchangeFilterFunction(openTracingConfigurationProperties.headers)
}
}
OpenTracingConfigurationProperties
#Configuration
#ConfigurationProperties("opentracing")
class OpenTracingConfigurationProperties {
lateinit var headers: Set<String>
}
application.yml
opentracing:
headers:
- x-request-id
- x-b3-traceid
- x-b3-spanid
- x-b3-parentspanid
- x-b3-sampled
- x-b3-flags
- x-ot-span-context
I needed to pass x-request-id header to a downstream service in my application. Achieved this by adding WebFilter that writes x-request-id to a reactor context
class ContextWebFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val headers = exchange.request.headers
val xRequestId = headers[X_REQUEST_ID]?.firstOrNull() ?: ""
val requestId = xRequestId.ifBlank { UUID.randomUUID().toString() }
return chain
.filter(exchange)
.contextWrite { it.put(X_REQUEST_ID, requestId) }
}
companion object {
const val X_REQUEST_ID = "X-REQUEST-ID"
}
}
and updating WebClient with ExchangeFilterFunction that updates outgoing request
WebClient.builder()
.filter(
ExchangeFilterFunction.ofRequestProcessor { request ->
Mono.deferContextual { context ->
val xRId = context.getOrDefault<String>("X-REQUEST-ID", "")
logger.debug("Set X-REQUEST-ID={} as a header to outgoing call", xRId)
Mono.just(
ClientRequest.from(request)
.header("X-REQUEST-ID", xRId)
.build()
)
}
}
)
.baseUrl("http://localhost:8080")
.build()

Retrieve path variable on Spring Boot WebFlux (functional approach)

Let's say I have this router definition:
#Component
class PersonRouter(private val handler: PersonHandler) {
#Bean
fun router(): RouterFunction<ServerResponse> = router {
("/api/people" and accept(MediaType.APPLICATION_JSON_UTF8)).nest {
GET("/{id}") { handler.findById(it) }
}
}
And then this handler:
#Component
#Transactional
class PersonHandler(private val repository: PersonRepository) {
private companion object : KLogging()
#Transactional(readOnly = true)
fun findById(req: ServerRequest): Mono<ServerResponse> {
logger.info { "${req.method()} ${req.path()}" }
val uuid = ? // req.pathContainer().elements().last().value()
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(repository.findById(uuid)))
.switchIfEmpty(ServerResponse.notFound().build())
}
}
How do I access the identifier (what would be a #PathVariable id: String on a typical #RestController) from ServerRequest without doing black magic with regular expressions, string-heavy-lifting, and such things?
Ah! Found it!
It is by doing: req.pathVariable("id")
It was there all the time...in the official Spring Framework (Web Reactive) documentation!

How to intercept requests by handler method in Spring WebFlux

I've got following interceptor in Spring MVC that checks if user can access handler method:
class AccessInterceptor : HandlerInterceptorAdapter() {
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any?): Boolean {
val auth: Auth =
(if (method.getAnnotation(Auth::class.java) != null) {
method.getAnnotation(Auth::class.java)
} else {
method.declaringClass.getAnnotation(Auth::class.java)
}) ?: return true
if (auth.value == AuthType.ALLOW) {
return true
}
val user = getUserFromRequest(request) // checks request for auth token
// and checking auth for out user in future.
return renderError(403, response)
In my Controller I do annotate methods, like this:
#GetMapping("/foo")
#Auth(AuthType.ALLOW)
fun doesntNeedAuth(...) { ... }
#GetMapping("/bar")
#Auth(AuthType.ADMIN)
fun adminMethod(...) { ... }
In case if user has wrong token or no permissions, error is being returned.
Is it possible to do this in Spring WebFlux with annotation-style controllers?
My implementation, w/o using toFuture().get() which is potentially blocking.
#Component
#ConditionalOnWebApplication(type = Type.REACTIVE)
public class QueryParameterValidationFilter implements WebFilter {
#Autowired
private RequestMappingHandlerMapping handlerMapping;
#NonNull
#Override
public Mono<Void> filter(#NonNull ServerWebExchange exchange, #NonNull WebFilterChain chain) {
return handlerMapping.getHandler(exchange)
.doOnNext(handler -> validateParameters(handler, exchange))
.then(chain.filter(exchange));
}
private void validateParameters(Object handler, ServerWebExchange exchange) {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Set<String> expectedQueryParams = Arrays.stream(handlerMethod.getMethodParameters())
.map(param -> param.getParameterAnnotation(RequestParam.class))
.filter(Objects::nonNull)
.map(RequestParam::name)
.collect(Collectors.toSet());
Set<String> actualQueryParams = exchange.getRequest().getQueryParams().keySet();
actualQueryParams.forEach(actual -> {
if (!expectedQueryParams.contains(actual)) {
throw new InvalidParameterException(ERR_MSG, actual);
}
});
}
}
}
To solve that problem I would most probably use:
A Spring Reactive Web WebFilter from the WebHandler API to intercept the incoming request
The RequestMappingHandlerMapping to retrieve the method which handles the current request
#Autowired
RequestMappingHandlerMapping requestMappingHandlerMapping;
...
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
...
HandlerMethod handler = (HandlerMethod) requestMappingHandlerMapping.getHandler(exchange).toProcessor().peek();
//your logic
}
#Component
class AuditWebFilter(
private val requestMapping: RequestMappingHandlerMapping
): WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
// if not to call - then exchange.attributes will be empty
// so little early initializate exchange.attributes by calling next line
requestMapping.getHandler(exchange)
val handlerFunction = exchange.attributes.get(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE) as HandlerMethod
val annotationMethod = handlerFunction.method.getAnnotation(MyAnnotation::class.java)
// annotationMethod proccesing here
}
}
In newer versions of Spring the .toProcessor() call is deprecated. What worked for me is to use .toFuture().get() instead:
if(requestMappingHandlerMapping.getHandler(exchange).toFuture().get() instanceof HandlerMethod handlerMethod) { ... }
Unfortunately this requires handling of checked exceptions so the code will be a bit less readable but at least not deprecated anymore.

Resources