Spring 5 Reactive - WebExceptionHandler is not getting called - spring-boot

I have tried all 3 solutions suggested in what is the right way to handle errors in spring-webflux, but WebExceptionHandler is not getting called. I am using Spring Boot 2.0.0.M7. Github repo here
#Configuration
class RoutesConfiguration {
#Autowired
private lateinit var testService: TestService
#Autowired
private lateinit var globalErrorHandler: GlobalErrorHandler
#Bean
fun routerFunction():
RouterFunction<ServerResponse> = router {
("/test").nest {
GET("/") {
ServerResponse.ok().body(testService.test())
}
}
}
}
#Component
class GlobalErrorHandler() : WebExceptionHandler {
companion object {
private val log = LoggerFactory.getLogger(GlobalErrorHandler::class.java)
}
override fun handle(exchange: ServerWebExchange?, ex: Throwable?): Mono<Void> {
log.info("inside handle")
/* Handle different exceptions here */
when(ex!!) {
is ClientException -> exchange!!.response.statusCode = HttpStatus.BAD_REQUEST
is Exception -> exchange!!.response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR
}
return Mono.empty()
}
}
UPDATE:
When I change Spring Boot version to 2.0.0.M2, the WebExceptionHandler is getting called. Do I need to do something for 2.0.0.M7?
SOLUTION:
As per Brian's suggestion, it worked as
#Bean
#Order(-2)
fun globalErrorHandler() = GlobalErrorHandler()

You can provide your own WebExceptionHandler, but you have to order it relatively to others, otherwise they might handle the error before yours get a chance to try.
the DefaultErrorWebExceptionHandler provided by Spring Boot for error handling (see reference documentation) is ordered at -1
the ResponseStatusExceptionHandler provided by Spring Framework is ordered at 0
So you can add #Order(-2) on your error handling component, to order it before the existing ones.

An error response should have standard payload info. This can be done by extending AbstractErrorWebExceptionHandler
ErrorResponse: Data Class
data class ErrorResponse(
val timestamp: String,
val path: String,
val status: Int,
val error: String,
val message: String
)
ServerResponseBuilder: 2 different methods to build an error response
default: handle standard errors
webClient: handle webClient exceptions (WebClientResponseException), not for this case
class ServerResponseBuilder(
private val request: ServerRequest,
private val status: HttpStatus) {
fun default(): Mono<ServerResponse> =
ServerResponse
.status(status)
.body(BodyInserters.fromObject(ErrorResponse(
Date().format(),
request.path(),
status.value(),
status.name,
status.reasonPhrase)))
fun webClient(e: WebClientResponseException): Mono<ServerResponse> =
ServerResponse
.status(status)
.body(BodyInserters.fromObject(ErrorResponse(
Date().format(),
request.path(),
e.statusCode.value(),
e.message.toString(),
e.responseBodyAsString)))
}
GlobalErrorHandlerConfiguration: Error handler
#Configuration
#Order(-2)
class GlobalErrorHandlerConfiguration #Autowired constructor(
errorAttributes: ErrorAttributes,
resourceProperties: ResourceProperties,
applicationContext: ApplicationContext,
viewResolversProvider: ObjectProvider<List<ViewResolver>>,
serverCodecConfigurer: ServerCodecConfigurer) :
AbstractErrorWebExceptionHandler(
errorAttributes,
resourceProperties,
applicationContext
) {
init {
setViewResolvers(viewResolversProvider.getIfAvailable { emptyList() })
setMessageWriters(serverCodecConfigurer.writers)
setMessageReaders(serverCodecConfigurer.readers)
}
override fun getRoutingFunction(errorAttributes: ErrorAttributes?): RouterFunction<ServerResponse> =
RouterFunctions.route(RequestPredicates.all(), HandlerFunction<ServerResponse> { response(it, errorAttributes) })
private fun response(request: ServerRequest, errorAttributes: ErrorAttributes?): Mono<ServerResponse> =
ServerResponseBuilder(request, status(request, errorAttributes)).default()
private fun status(request: ServerRequest, errorAttributes: ErrorAttributes?) =
HttpStatus.valueOf(errorAttributesMap(request, errorAttributes)["status"] as Int)
private fun errorAttributesMap(request: ServerRequest, errorAttributes: ErrorAttributes?) =
errorAttributes!!.getErrorAttributes(request, false)
}

Related

Error when mocking a repository in unit test of a spring webflux application (using JUnit and Mockito)?

I am yet to find a solution to this.
My Test class:
#WebFluxTest(controllers = {PatronController.class})
#Import({PatronService.class}) //to satisfy PatronController dependency.
#ExtendWith({PatronParameterResolver.class})
class PatronFunctionsSpec {
private Patron patron;
private Mono<Patron> patronMono;
private final PatronService patronService = Mockito.mock(PatronService.class);
#MockBean
private PatronRepository patronRepository;
#Autowired
private WebTestClient client;
#BeforeEach
void init(Patron injectedPatron) {
patron = injectedPatron;
patronMono = Mono.just(patron);
}
//Patron Story: patron wants to create an account with us
#Nested
#DisplayName("Creating a patron.")
class CreatingPatron {
#Test
#DisplayName("PatronService.create() returns success msg in Response obj after creating patron.")
void getResponseObjFromServiceCreate() {
Flux<Patron> patronFlux = Flux.from(patronMono);
Mockito.when(patronRepository.saveAll(patronMono)).thenReturn(patronFlux);
PatronService patronService = new PatronService(patronRepository);
Mono<Response> responseStream = patronService.create(Mono.just(patron));
Mono<Response> expectedResponseStream = Mono.just(new Response("Welcome, patron. Can't show emojis yet -- sorry."));
assertEquals(expectedResponseStream, responseStream);
}
}
}
See my PatronService class with its code:
#Service
public class PatronService {
private final PatronRepository patronRepository;
public PatronService(PatronRepository patronRepository) {
this.patronRepository = patronRepository;
}
/**
*
* persists patron via PatronRepo
*/
public Mono<Response> create(Mono<Patron> patronMono) {
patronRepository.saveAll(patronMono).subscribe();
return Mono.just(new Response("Welcome, patron. Can't find the emojis yet -- sorry."));
}
}
I am testing the PatronService's create(), so need to mock and stub PatronRepository and its function respectively. But the problem is: after running the test case, I get this exception:
java.lang.NullPointerException: Cannot invoke "reactor.core.publisher.Flux.subscribe()" because "patronFlux" is null
at com.budgit.service.PatronService.create(PatronService.java:26)
How can I fix this?

Spring RSocket Security JWT Access Denied Error

I am trying to use Spring Boot RSocket with Security using JWT Tokens. It is giving me an Access Denied error with no other useful information to help debug with?
Access Denied.
ApplicationErrorException (0x201): Access Denied at
app//io.rsocket.exceptions.Exceptions.from(Exceptions.java:76) at
app//io.rsocket.core.RSocketRequester.handleFrame(RSocketRequester.java:261)
at
app//io.rsocket.core.RSocketRequester.handleIncomingFrames(RSocketRequester.java:211)
at
app//reactor.core.publisher.LambdaSubscriber.onNext(LambdaSubscriber.java:160)
at
app//io.rsocket.core.ClientServerInputMultiplexer$InternalDuplexConnection.onNext(ClientServerInputMultiplexer.java:248)
at
app//io.rsocket.core.ClientServerInputMultiplexer.onNext(ClientServerInputMultiplexer.java:129)
at
app//io.rsocket.core.ClientServerInputMultiplexer.onNext(ClientServerInputMultiplexer.java:48)
at
app//reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
at
app//reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:364)
at
app//reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:404)
at
app//reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:725)
at
app//reactor.netty.http.client.WebsocketClientOperations.onInboundNext(WebsocketClientOperations.java:161)
at
app//reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at
app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at
app//io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:327)
at
app//io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:299)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at
app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at
app//io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at
app//io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at
app//io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at
app//io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722)
at
app//io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658)
at
app//io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584)
at app//io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496)
at
app//io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
at
app//io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at
app//io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base#11.0.14/java.lang.Thread.run(Thread.java:834)
Security Config file
#Configuration
#EnableRSocketSecurity
#EnableReactiveMethodSecurity
class SecurityConfig {
#Bean
fun authorization(rsocketSecurity: RSocketSecurity): PayloadSocketAcceptorInterceptor {
val security: RSocketSecurity =
rsocketSecurity.authorizePayload { authorize: RSocketSecurity.AuthorizePayloadsSpec ->
authorize
.anyRequest().authenticated()
.anyExchange().permitAll()
}
.jwt { jwtSpec ->
jwtSpec.authenticationManager(jwtReactiveAuthenticationManager(jwtDecoder()))
}
return security.build()
}
#Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return TokenUtils.jwtAccessTokenDecoder()
}
#Bean
fun jwtReactiveAuthenticationManager(decoder: ReactiveJwtDecoder): JwtReactiveAuthenticationManager {
val converter = JwtAuthenticationConverter()
val authoritiesConverter = JwtGrantedAuthoritiesConverter()
authoritiesConverter.setAuthorityPrefix("ROLE_")
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter)
val manager = JwtReactiveAuthenticationManager(decoder)
manager.setJwtAuthenticationConverter(ReactiveJwtAuthenticationConverterAdapter(converter))
return manager
}
#Bean
fun rsocketMessageHandler() = RSocketMessageHandler() .apply {
argumentResolverConfigurer.addCustomResolver(AuthenticationPrincipalArgumentResolver())
routeMatcher = PathPatternRouteMatcher()
rSocketStrategies = rsocketStrategies()
}
#Bean
fun rsocketStrategies() = RSocketStrategies.builder()
.routeMatcher(PathPatternRouteMatcher())
.build()
}
Message Controller file
#MessageMapping("api.v1.messages")
#Controller
class MessageController {
#MessageMapping("stream")
suspend fun receive(
#Payload inboundMessages: Flow<String>,
#AuthenticationPrincipal jwt: String
) {
println("MessageController: jwt: $jwt")
println("MessageController: inbound message: " + inboundMessages.first())
}
}
Testing using MessageControllerTest file
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MessageControllerTest(
#Autowired val rsocketBuilder: RSocketRequester.Builder,
#LocalServerPort val serverPort: Int
) {
#ExperimentalTime
#ExperimentalCoroutinesApi
#Test
fun `test that messages API streams latest messages`() {
val admin = HelloUser(userId = "9527", password = "password", role = HelloRole.ADMIN)
val token: UserToken = TokenUtils.generateAccessToken(admin)!!
val authenticationMimeType: MimeType =
MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string)
runBlocking {
val rSocketRequester = rsocketBuilder.websocket(URI("ws://localhost:${serverPort}/rsocket"))
launch {
rSocketRequester.route("api.v1.messages.stream")
.metadata(token.token!!, authenticationMimeType)
.dataWithType(flow {
emit(
"Hey from test class"
)
})
.retrieveFlow<Void>()
.collect()
}
}
}
}
I've add the rest of the code example I did to GitHub https://github.com/CJMobileApps/rsocket-jwt-security-example
I figured it out. RSocket currently has a bug or just bad documentation. The MimeType for JWTs BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE says that it is deprecated and to use MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string) instead, however that does not work.
When passing tokens continue using/use BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE along with the token string.
.metadata(token.token!!, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)

How to write test cases for custom ErrorAttributes in spring boot

I have updated the spring boot version to 2.6.4 and related other dependencies and got error in getErrorAttributes() method because of changes in its 2nd arguments type from Boolean to ErrorAttributeOptions
Custom ErrorAtttributes class:
#Component
class CustomErrorAttributes<T : Throwable> :DefaultErrorAttributes() {
override fun getErrorAttributes( request: ServerRequest , options: ErrorAttributeOptions ): MutableMap<String, Any> { // changes made here in 2nd parameter
val errorAttributes = super.getErrorAttributes(request, options) // throwing exception here
val status = (errorAttributes as MutableMap<String,Any>).getOrDefault(STATUS_KEY,null)
if(status != null && status as Int == HttpStatus.INTERNAL_SERVER_ERROR.value()){
errorAttributes.replace(MESSAGE_KEY, INTERNAL_SERVER_ERROR_MESSAGE)
}
return errorAttributes
}
}
Test method
private val internalError = "An unexpected error occurred"
#Mock private lateinit var request : ServerRequest
#Test
fun `For Internal Error`(){
var result : MutableMap<String,Any> = customErrorAttributes.getErrorAttributes(request, options) // It was working earlier version as we pass false in 2nd arguments
assertThat(result["message"]).isEqualTo(internalError)
}

How to wire #Configuration files from Unit Test - Spring Boot + Kotlin

I have an application.yml with some configuration properties required by my application.
SF:
baseurl: https://xxxxx
case:
recordTypeId: 0124a0000004Ifb
application:
recordTypeId: 0125P000000MkDa
address:
personal:
recordTypeId: 0125P000000MnuO
business:
recordTypeId: 0125P000000MnuT
I have defined a configuration class to read those properties as follows:
#Configuration
class SFProperties(
#Value("\${sf.case.recordTypeId}") val caseRecordTypeId: String,
#Value("\${sf.application.recordTypeId}") val applicationRecordTypeId: String,
#Value("\${sf.address.personal.recordTypeId}") val addressPersonalRecordTypeId:String,
#Value("\${sf.address.business.recordTypeId}") val addressBusinessRecordTypeId: String
)
The class is wired within a service without any issues,
#Service
class SFClientManagementServiceImpl( val webClientBuilder: WebClient.Builder):
ClientManagementService {
....
#Autowired
lateinit var sfProperties: SFProperties
override fun createCase(caseRequest: CaseRequestDto): Mono<CaseResponseDto> {
...
var myValue= sfProperties.caseRecordTypeId
....
}
}
When trying to test this service, I get a "lateinit property sfProperties has not been initialized" exception:
The test looks as follows:
#SpringBootTest(classes = [SFProperties::class])
class SalesforceClientManagementServiceImplTests {
#Autowired
open lateinit var sfProperties: SFProperties
#Test
fun `createCase should return case id when case is created`() {
val clientResponse: ClientResponse = ClientResponse
.create(HttpStatus.OK)
.header("Content-Type", "application/json")
.body(ObjectMapper().writeValueAsString(Fakes().GetFakeCaseResponseDto())).build()
val shortCircuitingExchangeFunction = ExchangeFunction {
Mono.just(clientResponse)
}
val webClientBuilder: WebClient.Builder = WebClient.builder().exchangeFunction(shortCircuitingExchangeFunction)
val sfClientManagementServiceImpl =
SFClientManagementServiceImpl(webClientBuilder)
var caseResponseDto =
salesforceClientManagementServiceImpl.createCase(Fakes().GetFakeCaseRequestDto())
var response = caseResponseDto.block()
if (response != null) {
assertEquals(Fakes().GetFakeCaseResponseDto().id, response.id)
}
}
I have tried many other annotations on the Test class but without success, I would appreciate any ideas.

Async Spring Boot using Kotlin not working

I'm trying to create a Spring Service that performs an operation asynchronously and returns a ListenableFuture. I want the failure callback to be triggered when the operation fails - my attempt to do this is to use AsyncResult.forExecutionException as seen below:
#Service
open class UserClientService {
#Async
fun fetchUser(email: String): ListenableFuture<User> {
val uri = buildUri(email)
val headers = buildHeaders()
try {
val result = restTemplate.exchange(uri, HttpMethod.GET, HttpEntity<Any>(headers), User::class.java)
return AsyncResult.forValue(result.body)
} catch (e: RestClientException) {
return AsyncResult.forExecutionException(e)
}
}
}
The entry-point:
#SpringBootApplication
#EnableAsync
open class UserProxyApplication
fun main(args: Array<String>) {
SpringApplication.run(UserProxyApplication::class.java, *args)
}
The Spring RestController implementation is as follows:
#RestController
#RequestMapping("/users")
class UserController #Autowired constructor(
val client: UserClientService
) {
#RequestMapping(method = arrayOf(RequestMethod.GET))
fun getUser(#RequestParam(value = "email") email: String): DeferredResult<ResponseEntity<User>> {
val result = DeferredResult<ResponseEntity<User>>(TimeUnit.SECONDS.toMillis(10))
client.fetchUser(email).addCallback(
{ success -> result.setResult(ResponseEntity.ok(success)) },
{ failure -> result.setResult(ResponseEntity(HttpStatus.NOT_FOUND)) }
)
return result;
}
}
Problem is that the failure callback in the UserController is never triggered when an exception is thrown in the UserClientService REST call. Instead, the success callback is triggered with success argument being null.
In Kotlin, I can check if success is null by using success!! - this throws an exception that then does trigger the failure callback with failure argument being the NPE.
Question is how can I trigger the failure callback in the UserController when an exception has occurred in the UserClientService?
Update A it seems that everything is executed on the same thread "http-nio-8080-exec-XXX" regardless of whether I use #Async or not -- see comments.
This all works if:
A) the method fetchUser is declared open, i.e. not final so that Spring can proxy the call
...or...
B) you create an interface IUserClientService and use that in the constructor of the UserController:
interface IUserClientService {
fun fetchUser(email: String): ListenableFuture<User>
}
Now the UserClientService implements the interface:
#Service
open class UserClientService : IUserClientService {
#Async
override fun fetchUser(email: String): ListenableFuture<User> {
// ... rest as shown in question ...
And finally the UserController:
#RestController
#RequestMapping("/users")
class UserController #Autowired constructor(
val client: IUserClientService
) {
#RequestMapping(method = arrayOf(RequestMethod.GET))
fun getUser(#RequestParam(value = "email") email: String): DeferredResult<ResponseEntity<User>> {
// ... rest as shown in question ...
Not sure if this is because I'm using Kotlin. The examples that I've seen don't require implementing an interface.

Resources