Spring webflux error handler: How to get the reactor context of the request in the error handler? - spring-webflux

Spring boot 2.1.5 Project Reactor 3.2.9
In my webflux project, I extensively use the reactor contexts in order to pass around some values.
My purpose here is to be able to get the context inside of the Exception handler.
A simple example:
#Component
#Order(-2)
public class GlobalErrorWebExceptionHandler extends
AbstractErrorWebExceptionHandler {
public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ApplicationContext applicationContext, ServerCodecConfigurer configurer) {
super(errorAttributes, resourceProperties, applicationContext);
this.setMessageWriters(configurer.getWriters());
}
#Override
protected RouterFunction<ServerResponse> getRoutingFunction(
ErrorAttributes errorAttributes) {
return RouterFunctions
.route(RequestPredicates.all(), request -> {
Throwable error = errorAttributes.getError(request);
return ServerResponse.status(500).syncBody(error.getMessage()).doOnEach(serverResponseSignal -> {
//Here the context is empty because i guess i created a new flux
System.out.println("What is in my context ? " + serverResponseSignal.getContext());
System.out.println("What is my exception ? " + error);
});
});
}
}
I am not sure how to achieve that goal in a clean way with reactor. Anyone an idea ?

I found a trick to be able to achieve that. It does not sound clean but it seems to work.
In a filter, I keep the subscribed context into a request attribute:
#Component
public class MdcWebFilter implements WebFilter {
#NotNull
#Override
public Mono<Void> filter(#NotNull ServerWebExchange serverWebExchange,
WebFilterChain webFilterChain) {
Mono<Void> filter = webFilterChain.filter(serverWebExchange);
return filter
.subscriberContext((context) -> {
//This code is executed before the query
Context contextTmp = context.put("whatever", "whichever");
//I save the context in an attribute attribute
serverWebExchange.getAttributes().put("context", contextTmp);
return contextTmp;
});
}
}
Then after that it is possible to get it from the reactive error handler:
#Component
#Order(-2)
public class GlobalErrorWebExceptionHandler extends
AbstractErrorWebExceptionHandler {
public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ApplicationContext applicationContext, ServerCodecConfigurer configurer) {
super(errorAttributes, resourceProperties, applicationContext);
this.setMessageWriters(configurer.getWriters());
}
#Override
protected RouterFunction<ServerResponse> getRoutingFunction(
ErrorAttributes errorAttributes) {
return RouterFunctions
.route(RequestPredicates.all(), request -> {
Throwable error = errorAttributes.getError(request);
//The context will be visible in the whole error handling flow
return ServerResponse.status(500).syncBody(error.getMessage())
.subscriberContext((Context) request.attribute("context").orElse(Context.empty())));
});
}
}

Related

Eventbus subscribers not registering fully

My debug output is showing this;
D/EventBus: No subscribers registered for event class com.me.MyEvent
My concern is the index is not working fully and as a result EventBus is using reflection to find the subscriber/s.
The event is published from button click inside a RecyclerView's adapter, and is received by the Fragment containing the RecyclerView. Send and receive work fine. As I said, my concern is reflection is being used.
The index seems to be built correctly. My build.gradle
def eventbus_version = '3.3.1'
implementation "org.greenrobot:eventbus:$eventbus_version"
kapt "org.greenrobot:eventbus-annotation-processor:$eventbus_version"
and
kapt {
arguments {
arg('eventBusIndex', 'com.me.MyEventBusIndex')
}
Here's part of the generated index;
public class MyEventBusIndex implements SubscriberInfoIndex {
private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;
static {
SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();
putIndex(new SimpleSubscriberInfo(MyFragment.class, true, new SubscriberMethodInfo[] {
new SubscriberMethodInfo("handleEvent", com.me.MyEvent.class,
ThreadMode.POSTING, 0, true),
}));
}
and some fragment code;
override fun onStart() {
super.onStart()
EventBus.builder().addIndex(MyEventBusIndex()).build().register(this)
}
override fun onStop() {
EventBus.getDefault().unregister(this)
super.onStop()
}
and finally a fragment method;
#Subscribe(sticky = true)
fun handleEvent(event: MyEvent) {
//dostuff
EventBus.getDefault().removeStickyEvent(event)
}

Contextual logging (corelation id in headers ) with webflux

I need some params from the headers inside my coroutine in my controller, and log them as a corelation id for my requests.
Is it possible to use webflux / kotlin coroutines in controller AND to do contextual logging with the params in the header ?
I know Webflux can use WebFilter to intercept headers and log them or modify them, but can it be sent to the coroutine it will trigger ?
#RestController
class ItemController(private val itemRepository: ItemRepository) {
#GetMapping("/")
suspend fun findAllItems(): List<Item> =
// do stuff
logger.log("Corelation id is : " + myCorelationIdHeaderParam) // that's the param i need
return itemService.findAll()
}
Any context you set in the webfilter can be accessed by using subscriberContext down the line in your controllers/services.
Below is an example using Java. You can use similar logic in your Kotlin code:
Your filter: (Here you are setting the header value "someHeaderval" in your "myContext" )
public class MyFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String someHeaderval = request.getHeaders().get("someHeader").get(0);
return chain.filter(exchange).subscriberContext(context -> {
return context.put("myContext",someHeaderval);
});;
}
}
Now you can use this context anywhere:
#GetMapping(value = "/myGetApi")
public Mono<String> sampleGet() {
return Mono.subscriberContext()
.flatMap(context -> {
String myHeaderVal = (String)context.get("myContext");
//do logging with this header value
return someService.doSomething(myHeaderVal);
});
}
It turns out you can access the ReactorContext from the CoroutineContext with
coroutineContext[ReactorContext]
Here is my code :
#Component
class MyWebFilter : WebFilter {
val headerKey = "correlation-token-key"
val contextKey = "correlationId"
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val headers: HttpHeaders = exchange.request.headers
return chain.filter(exchange)
.subscriberContext(Context.of(contextKey, headers[headerKey] ?: "unidentified"))
}
}
and the controller part (most important for Kotlin users) :
#RestController
class ItemController(private val itemRepository: ItemRepository) {
#GetMapping("/")
suspend fun findAllItems(): List<Item> =
// do stuff
logger.log("Correlation id of request is : " + coroutineContext[ReactorContext]?.context?.get<List<String>>("correlationId")?.firstOrNull())
return itemService.findAll()
}

Transactions with ReactiveCrudRepository with spring-data-r2dbc

I'm trying to implement transactions with spring-data-r2dbc repositories in combination with the TransactionalDatabaseClient as such:
class SongService(
private val songRepo: SongRepo,
private val databaseClient: DatabaseClient
){
private val tdbc = databaseClient as TransactionalDatabaseClient
...
...
fun save(song: Song){
return tdbc.inTransaction{
songRepo
.save(mapRow(song, albumId)) //Mapping to a row representation
.delayUntil { savedSong -> tdbc.execute.sql(...).fetch.rowsUpdated() } //saving a many to many relation
.map(::mapSong) //Mapping back to actual song and retrieve the relationship data.
}
}
}
I currently have a config class (annotated with #Configuration and #EnableR2dbcRepositories) that extends from AbstractR2dbcConfiguration. In here I override the databaseClient method to return a TransactionalDatabaseClient. This should be the same instance as in the SongService class.
When running the code in a test with just subscribing and printing, I get org.springframework.transaction.NoTransactionException: ReactiveTransactionSynchronization not active and the relationship data is not returned.
When using project Reactors stepverifier though, i get java.lang.IllegalStateException: Connection is closed. Also in this case, the relationship data is not returned.
Just for the record, I have seen https://github.com/spring-projects/spring-data-r2dbc/issues/44
Here is a working Java example:
#Autowired TransactionalDatabaseClient txClient;
#Autowired Mono<Connection> connection;
//You Can also use: #Autowired Mono<? extends Publisher> connectionPublisher;
public Flux<Void> example {
txClient.enableTransactionSynchronization(connection);
// Or, txClient.enableTransactionSynchronization(connectionPublisher);
Flux<AuditConfigByClub> audits = txClient.inTransaction(tx -> {
txClient.beginTransaction();
return tx.execute().sql("SELECT * FROM audit.items")
.as(Item.class)
.fetch()
.all();
}).doOnTerminate(() -> {
txClient.commitTransaction();
});
txClient.commitTransaction();
audits.subscribe(item -> System.out.println("anItem: " + item));
return Flux.empty()
}
I just started reactive so not too sure what I'm doing with my callbacks haha. But I decided to go with TransactionalDatabaseClient over DatabaseClient or Connection since I'll take all the utility I can get while R2dbc is in its current state.
In your code did you actually instantiate a Connection object? If so I think you would have done it in your configuration. It can be utilized throughout the app the same as DatabaseClient, but it is slightly more intricate.
If not:
#Bean
#Override // I also used abstract config
public ConnectionFactory connectionFactory() {
...
}
#Bean
TransactionalDatabaseClient txClient() {
...
}
//TransactionalDatabaseClient will take either of these as arg in
//#enableTransactionSynchronization method
#Bean
public Publisher<? extends Connection> connectionPublisher() {
return connectionFactory().create();
}
#Bean
public Mono<Connection> connection() {
return = Mono.from(connectionFactory().create());
}
If you are having problems translating to Kotlin, there is an alternative way to enable synchronization that could work:
// From what I understand, this is a useful way to move between
// transactions within a single subscription
TransactionResources resources = TransactionResources.create();
resources.registerResource(Resource.class, resource);
ConnectionFactoryUtils
.currentReactiveTransactionSynchronization()
.subscribe(currentTx -> sync.registerTransaction(Tx));
Hope this translates well for Kotlin.

Is there a way to specify webSessionManager when using WebTestClientAutoConfiguration?

I am using Webflux in Spring Boot 2.0.3.RELEASE to create REST API. With that implementation, I customize and use the webSessionManager as below.
#EnableWebFluxSecurity
#Configuration
class SecurityConfiguration {
#Bean
fun webSessionManager(): WebSessionManager {
return DefaultWebSessionManager().apply {
sessionIdResolver = HeaderWebSessionIdResolver().apply {
headerName = "X-Sample"
}
sessionStore = InMemoryWebSessionStore()
}
}
// ...
}
And in order to test the REST API, I created a test code as follows. (addUser and signin are extension functions.)
#RunWith(SpringRunner::class)
#SpringBootTest
#AutoConfigureWebTestClient
#FixMethodOrder(MethodSorters.NAME_ASCENDING)
class UserTests {
#Autowired
private lateinit var client: WebTestClient
#Test
fun testGetUserInfo() {
client.addUser(defaultUser)
val sessionKey = client.signin(defaultUser)
client.get().uri(userPath)
.header("X-Sample", sessionKey)
.exchange()
.expectStatus().isOk
.expectBody()
.jsonInStrict("""
{
"user": {
"mail_address": "user#example.com"
}
}
""".trimIndent())
}
// ...
}
The test failed. It is refused by authorization. However, if I start the server and run it from curl it will succeed in the authorization.
After investigating the cause, it turned out that org.springframework.test.web.reactive.server.AbstractMockServerSpec set webSessionManager to DefaultWebSessionManager. Default is used, not the webSessionManager I customized. For this reason, it could not get the session ID.
AbstractMockServerSpec.java#L41
AbstractMockServerSpec.java#L72-L78
How can I change the webSessionManager of AbstractMockServerSpec?
Also, I think that it is better to have the following implementation, what do you think?
abstract class AbstractMockServerSpec<B extends WebTestClient.MockServerSpec<B>>
implements WebTestClient.MockServerSpec<B> {
// ...
private WebSessionManager sessionManager = null;
// ...
#Override
public WebTestClient.Builder configureClient() {
WebHttpHandlerBuilder builder = initHttpHandlerBuilder();
builder.filters(theFilters -> theFilters.addAll(0, this.filters));
if (this.sessionManager != null) {
builder.sessionManager(this.sessionManager);
}
this.configurers.forEach(configurer -> configurer.beforeServerCreated(builder));
return new DefaultWebTestClientBuilder(builder);
}
// ...
}
Spring Framework's AbstractMockServerSpec is providing a method to customize the WebSessionManager already.
Thanks for opening SPR-17094, this problem will be solved with that ticket - the AbstractMockServerSpec is already looking into the application context for infrastructure bits, this should check for a WebSessionManager as well.

What is the reason for twitter4j.StreamListner IllegalAccessError in Kotlin?

When implementing a twitter4j.StatusListner in Kotlin, I get the following IllegalAccessError and associated stack trace:
Exception in thread "main" java.lang.IllegalAccessError: tried to access class twitter4j.StreamListener from class rxkotlin.rxextensions.TwitterExampleKt$observe$1
at rxkotlin.rxextensions.TwitterExampleKt$observe$1.subscribe(TwitterExample.kt:50)
at io.reactivex.internal.operators.observable.ObservableCreate.subscribeActual(ObservableCreate.java:40)
at io.reactivex.Observable.subscribe(Observable.java:10700)
at io.reactivex.Observable.subscribe(Observable.java:10686)
at io.reactivex.Observable.subscribe(Observable.java:10615)
at rxkotlin.rxextensions.TwitterExampleKt.main(TwitterExample.kt:8)
Produced by the following code:
val twitterStream = TwitterStreamFactory().instance
// See https://stackoverflow.com/questions/37672023/how-to-create-an-instance-of-anonymous-interface-in-kotlin/37672334
twitterStream.addListener(object : StatusListener {
override fun onStatus(status: Status?) {
if (emitter.isDisposed) {
twitterStream.shutdown()
} else {
emitter.onNext(status)
}
}
override fun onException(e: Exception?) {
if (emitter.isDisposed) {
twitterStream.shutdown()
} else {
emitter.onError(e)
}
}
// Other overrides.
})
emitter.setCancellable { twitterStream::shutdown }
If I don't use Rx, it makes the exception a bit simpler:
twitterStream.addListener(object: twitter4j.StatusListener {
override fun onStatus(status: Status) { println("Status: {$status}") }
override fun onException(ex: Exception) { println("Error callback: $ex") }
// Other overrides.
})
Exception in thread "main" java.lang.IllegalAccessError: tried to access class twitter4j.StreamListener from class rxkotlin.rxextensions.TwitterExampleKt
at rxkotlin.rxextensions.TwitterExampleKt.main(TwitterExample.kt:14)
However, if I implement a Java wrapper function, no error is thrown and the behaviour is as expected:
Wrapper -
public class Twitter4JHelper {
public static void addStatusListner(TwitterStream stream, StatusListener listner) {
stream.addListener(listner);
}
}
Revised implementation -
val twitterStream = TwitterStreamFactory().instance
val listner = object: StatusListener {
override fun onStatus(status: Status?) {
if (emitter.isDisposed) {
twitterStream.shutdown()
} else {
emitter.onNext(status)
}
}
override fun onException(e: Exception?) {
if (emitter.isDisposed) {
twitterStream.shutdown()
} else {
emitter.onError(e)
}
}
// Other overrides.
}
Twitter4JHelper.addStatusListner(twitterStream, listner)
emitter.setCancellable { twitterStream::shutdown }
This revised solution comes from a blog post, which I think tries to explain the cause but Google translate is not being my friend. What is causing the IllegalAccessError? Is there a purely Kotlin based solution, or will I have to live with this workaround?
Yep that's not going to work.
addListener method takes a StreamListener param and StreamListener is non-public (package private). I would definitely raise a bug against Kotlin compiler for this.
The code Kotlin compiler generates is:
TwitterStream twitterStream = (new TwitterStreamFactory()).getInstance();
twitterStream.addListener((StreamListener)(new StatusListener() {
// ..overrides ...
}));
StatusListener already implements StreamListener so I don't see why the cast is required.
I worked around this by using a java utility class:
public class T4JCompat {
public static void addStatusListener(TwitterStream stream, StatusListener listener) {
stream.addListener(listener);
}
public static void removeStatusListener(TwitterStream stream, StatusListener listener) {
stream.removeListener(listener);
}
}
You can call these methods from Kotlin and things work as expected.