I've been doing some research using spring-webflux and I like to understand what should be the right way to handle errors using Router Functions.
I've created an small project to test a couple of scenarios, and I like to get feedback about it, and see what other people is doing.
So far what I doing is.
Giving the following routing function:
#Component
public class HelloRouter {
#Bean
RouterFunction<?> helloRouterFunction() {
HelloHandler handler = new HelloHandler();
ErrorHandler error = new ErrorHandler();
return nest(path("/hello"),
nest(accept(APPLICATION_JSON),
route(GET("/"), handler::defaultHello)
.andRoute(POST("/"), handler::postHello)
.andRoute(GET("/{name}"), handler::getHello)
)).andOther(route(RequestPredicates.all(), error::notFound));
}
}
I've do this on my handler
class HelloHandler {
private ErrorHandler error;
private static final String DEFAULT_VALUE = "world";
HelloHandler() {
error = new ErrorHandler();
}
private Mono<ServerResponse> getResponse(String value) {
if (value.equals("")) {
return Mono.error(new InvalidParametersException("bad parameters"));
}
return ServerResponse.ok().body(Mono.just(new HelloResponse(value)), HelloResponse.class);
}
Mono<ServerResponse> defaultHello(ServerRequest request) {
return getResponse(DEFAULT_VALUE);
}
Mono<ServerResponse> getHello(ServerRequest request) {
return getResponse(request.pathVariable("name"));
}
Mono<ServerResponse> postHello(ServerRequest request) {
return request.bodyToMono(HelloRequest.class).flatMap(helloRequest -> getResponse(helloRequest.getName()))
.onErrorResume(error::badRequest);
}
}
Them my error handler do:
class ErrorHandler {
private static Logger logger = LoggerFactory.getLogger(ErrorHandler.class);
private static BiFunction<HttpStatus,String,Mono<ServerResponse>> response =
(status,value)-> ServerResponse.status(status).body(Mono.just(new ErrorResponse(value)),
ErrorResponse.class);
Mono<ServerResponse> notFound(ServerRequest request){
return response.apply(HttpStatus.NOT_FOUND, "not found");
}
Mono<ServerResponse> badRequest(Throwable error){
logger.error("error raised", error);
return response.apply(HttpStatus.BAD_REQUEST, error.getMessage());
}
}
Here is the full sample repo:
https://github.com/LearningByExample/reactive-ms-example
Spring 5 provides a WebHandler, and in the JavaDoc, there's the line:
Use HttpWebHandlerAdapter to adapt a WebHandler to an HttpHandler. The WebHttpHandlerBuilder provides a convenient way to do that while also optionally configuring one or more filters and/or exception handlers.
Currently, the official documentation suggests that we should wrap the router function into an HttpHandler before booting up any server:
HttpHandler httpHandler = RouterFunctions.toHttpHandler(routerFunction);
With the help of WebHttpHandlerBuilder, we can configure custom exception handlers:
HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(toHttpHandler(routerFunction))
.prependExceptionHandler((serverWebExchange, exception) -> {
/* custom handling goes here */
return null;
}).build();
If you think, router functions are not the right place to handle exceptions, you throw HTTP Exceptions, that will result in the correct HTTP Error codes.
For Spring-Boot (also webflux) this is:
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
.
.
.
new ResponseStatusException(HttpStatus.NOT_FOUND, "Collection not found");})
spring securities AccessDeniedException will be handled correctly, too (403/401 response codes).
If you have a microservice, and want to use REST for it, this can be a good option, since those http exceptions are quite close to business logic, and should be placed near the business logic in this case. And since in a microservice you shouldn't have to much businesslogic and exceptions, it shouldn't clutter your code, too... (but of course, it all depends).
Why not do it the old fashioned way by throwing exceptions from handler functions and implementing your own WebExceptionHandler to catch 'em all:
#Component
class ExceptionHandler : WebExceptionHandler {
override fun handle(exchange: ServerWebExchange?, ex: Throwable?): Mono<Void> {
/* Handle different exceptions here */
when(ex!!) {
is NoSuchElementException -> exchange!!.response.statusCode = HttpStatus.NOT_FOUND
is Exception -> exchange!!.response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR
}
/* Do common thing like logging etc... */
return Mono.empty()
}
}
Above example is in Kotlin, since I just copy pasted it from a project I´m currently working on, and since the original question was not tagged for java anyway.
You can write a Global exception handler with custom response data and response code as follows. The code is in Kotlin. But you can convert it to java easily:
#Component
#Order(-2)
class GlobalWebExceptionHandler(
private val objectMapper: ObjectMapper
) : ErrorWebExceptionHandler {
override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {
val response = when (ex) {
// buildIOExceptionMessage should build relevant exception message as a serialisable object
is IOException -> buildIOExceptionMessage(ex)
else -> buildExceptionMessage(ex)
}
// Or you can also set them inside while conditions
exchange.response.headers.contentType = MediaType.APPLICATION_PROBLEM_JSON
exchange.response.statusCode = HttpStatus.valueOf(response.status)
val bytes = objectMapper.writeValueAsBytes(response)
val buffer = exchange.response.bufferFactory().wrap(bytes)
return exchange.response.writeWith(Mono.just(buffer))
}
}
A quick way to map your exceptions to http response status is to throw org.springframework.web.server.ResponseStatusException / or create your own subclasses...
Full control over http response status + spring will add a response body with the option to add a reason.
In Kotlin it could look as simple as
#Component
class MyHandler(private val myRepository: MyRepository) {
fun getById(req: ServerRequest) = req.pathVariable("id").toMono()
.map { id -> uuidFromString(id) } // throws ResponseStatusException
.flatMap { id -> noteRepository.findById(id) }
.flatMap { entity -> ok().json().body(entity.toMono()) }
.switchIfEmpty(notFound().build()) // produces 404 if not found
}
fun uuidFromString(id: String?) = try { UUID.fromString(id) } catch (e: Throwable) { throw BadRequestStatusException(e.localizedMessage) }
class BadRequestStatusException(reason: String) : ResponseStatusException(HttpStatus.BAD_REQUEST, reason)
Response Body:
{
"timestamp": 1529138182607,
"path": "/api/notes/f7b.491bc-5c86-4fe6-9ad7-111",
"status": 400,
"error": "Bad Request",
"message": "For input string: \"f7b.491bc\""
}
What I am currently doing is simply providing a bean my WebExceptionHandler :
#Bean
#Order(0)
public WebExceptionHandler responseStatusExceptionHandler() {
return new MyWebExceptionHandler();
}
The advantage than creating the HttpHandler myself is that I have a better integration with WebFluxConfigurer if I provide my own ServerCodecConfigurer for example or using SpringSecurity
Related
So I've been using Spring and Java for a while to build microservices. I am concerned by the way I am currently handling service layer results which uses "business exception"
Controller
#RestController
public class PurchaseController {
#Autowired
private PurchaseService purchaseService;
#PostMapping("/checkout")
public ResponseEntity<?> checkout(#RequestBody CheckoutRequest body) {
try {
SomeDTO dto = purchaseService.doCheckout(body);
return ResponseEntity.ok(dto);
}
catch (UnauthorizedException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}
catch (CustomBusinessException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}
}
Service
#Service
public class PurchaseService {
// ...
public DTO doCheckout(CheckoutRequest request) {
// this one calls another microservice
if (!isUserValid(request.userId)) {
// current handling of business rules violation (1)
throw new UnauthorizedException("User not valid");
}
if (request.total < 10) {
// current handling of business rules violation (2)
throw new CustomBusinessException("Minimum checkout at 20 dollars");
}
// ... do actual checkout
return new DTO(someDTOData);
}
}
I was comfortable at using this "pattern" because I do not need to "if" the business result in the controller level to return the appropriate HttpStatusCode, but since I've found some articles saying that exception is expensive specifically in Java, I doubt what I was doing is good for the long run.
Is there another correct way to gracefully handles the business result layer?
The problem with ResponseEntity in Spring is that they are typed with the result object you want to return when the endpoint is called successfully, so you can't return another body different from the happy path one, that in your case would be SameDTO. One way to address this issue is to use ? as the type of the response entity, as you have done but it is not the most recommended way.
So the best way to do this is precisely to use exceptions when there is a situation when you can't return the expected object and you have to return another object or status code, but instead of using a try-catch in the controller you should use an exception handler (Controller Advice) https://www.baeldung.com/exception-handling-for-rest-with-spring.
This controller advice would catch any exception thrown in your application and depending on the exception type it could return a different response class or status code without affecting the main controller. One example of how can be your controller advice would be:
#ControllerAdvice
public class ErrorHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleInternal(final RuntimeException ex) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ex.getMessage());
}
#ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<ResponseDto> identityClientException(UnauthorizedException e) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(e.getMessage());
}
#ExceptionHandler(CustomBusinessException.class)
public ResponseEntity<ResponseDto> identityClientException(CustomBusinessException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}
And your controller woulb be much more clean without exception handling logic:
#RestController
public class PurchaseController {
#Autowired
private PurchaseService purchaseService;
#PostMapping("/checkout")
public ResponseEntity<SomeDTO> checkout(#RequestBody CheckoutRequest body){
SomeDTO dto = purchaseService.doCheckout(body);
return ResponseEntity.ok(dto);
}
}
Given: I have a service which produces a Flowable<T>. This Flowable<T> can be empty.
I have a controller, which looks similar to this:
#Controller("/api}")
class ApiController constructor( private val myService: MyService) {
#Get("/")
#Produces(MediaType.APPLICATION_JSON)
fun getSomething(): Flowable<T> {
return myService.get()
}
}
What I want to achieve: when the flowable is empty -> throw a HttpStatusException(404).
Otherwise return the flowable with the data inside.
What I already tried
I tried different combinations of the following RxJava Operators:
doOnError
onErrorResumeNext
onErrorReturn
switchIfEmpty
...
What I experienced
None of the options produced a 404 in the Browser/Postman.
A couple of options are just doing "nothing". Which means, that the page is not loading in the browser.
Other options are creating "OK" (200) responses with empty bodies.
And some are creating a CompositeException...
Does someone have a hint for me?
Update: as suggested:
#Controller("/api")
class HelloController {
#Get("/")
#Produces(MediaType.APPLICATION_JSON)
fun get(): Flowable<String> {
return Flowable.empty<String>()
.switchIfEmpty {
it.onError(HttpStatusException(HttpStatus.NOT_FOUND,""))
}
}
}
This produces the following, when I call it with firefox:
HttpResponseStatus: 200
HttpContent: [
Yes, the closing bracet is missing!
A possible Solution is, to use Maybe instead of Flowable.
#Controller("/api")
class HelloController {
#Get("/")
#Produces(MediaType.APPLICATION_JSON)
fun get(): Maybe<String> {
return Flowable.empty<String>()
.toList()
.flatMapMaybe { x ->
if (x.size == 0)
Maybe.empty<String>()
else
Maybe.just(x)
}
}
}
}
It is not the best solution, but a working one.
I don't know Micronaut, but I think this may be what you want:
Flowable.empty<Int>()
.switchIfEmpty { it.onError(Exception()) } // or HttpStatusException(404) in your case
.subscribe({
println(it)
}, {
it.printStackTrace()
})
The Flowable is empty, and what you get downstream is the Exception created inside switchIfEmpty. Note that you have to call it.onError inside switchIfEmpty.
Have the following implementation of webclient :
public <T> WebClient.ResponseSpec sendRequest(HttpMethod method, String contentType, T body, String baseUrl, String path) {
try {
WebClient webClient = WebClient.builder().baseUrl(baseUrl).filter(logRequest()).build();
WebClient.ResponseSpec responseSpec = webClient.method(method)
.uri(path)
.header(HttpHeaders.CONTENT_TYPE, contentType)
.body(BodyInserters.fromObject(body))
.retrieve();
return responseSpec;
} catch (Exception e) {
throw new WebClientProcessingException("Exception when trying to execute request", e);
}
}
// This method returns filter function which will log request data
private static ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
LOGGER.info("Request: {} {} {}", clientRequest.method(), clientRequest.url(), clientRequest.body());
clientRequest.headers().forEach((name, values) -> values.forEach(value -> LOGGER.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
Also have the following code , which is creating user object and command which contains user object , then calling webclient to send an request
#Autowired
private BaseWebClient baseWebClient;
#Override
public void saveOrUpdateUser() {
UserPayload userPayload = new UserPayload();
userPayload.setUserId(111L);
userPayload.setCreatedAt(ZonedDateTime.now(DateTimeProps.systemTimeZone));
UserCommand userCommand = new UserCommand();
userCommand.setUser(userPayload);
baseWebClient.sendRequest(HttpMethod.POST, "application/json",
Stream.of(userCommand).collect(Collectors.toList()),
"http://localhost:8080",
"/users").onStatus(HttpStatus::isError, clientResponse -> {
throw new WebClientUserSaveOrUpdateeFailedException("Exception when trying to update user state")
.bodyToMono(String.class);
});
}
User payload :
#Data
#NoArgsConstructor
#AllArgsConstructor
public class UserPayload {
Long userId;
ZonedDateTime createdAt;
}
User command :
#Data
#NoArgsConstructor
#AllArgsConstructor
public class UserCommand {
#JsonProperty("user")
UserPayload user;
}
Json which is waiting for my other app (whom I am sending a request) :
[
{ "user":
{
"userId": 1,
"createdAt": "2019-05-16T08:24:46.412Z"
}
}
]
Using : Spring boot 2 , Lombok (for getter/setter) , gradle
When I'm trying to send a request nothing happens. No exception even.
I tried with very simple case as well the same issue.
One more note, is it possible to log body? I mean somehow see final json
I guess I am missing something general.
In Reactor, nothing happens until you subscribe. retrive() does not actually start the request. As you can see in the example, you should use one of the method to convert the ResponseSpec to a Publisher and then eventually subscribe to that publisher.
Depending on how you're using this method, you might be able to let Spring subscribe to the publisher instead. WebFlux supports reactive types in the model which means you can directly return a Mono from your RestController methods, for example.
I have a simple Micronaut- based "hello world" service that has a simple security built in (for the sake of testing and illustrating the Micronaut security). The controller code in the service that implements the hello service is provided below:
#Controller("/hello")
public class HelloController
{
public HelloController()
{
// Might put some stuff in in the future
}
#Get("/")
#Produces(MediaType.TEXT_PLAIN)
public String index()
{
return("Hello to the World of Micronaut!!!");
}
}
In order to test the security mechanism, I have followed the Micronaut tutorial instructions and created a security service class:
#Singleton
public class SecurityService
{
public SecurityService()
{
// Might put in some stuff in the future
}
Flowable<Boolean> checkAuthorization(HttpRequest<?> theReq)
{
Flowable<Boolean> flow = Flowable.fromCallable(()->{
System.out.println("Security Engaged!");
return(false); <== The tutorial says return true
}).subscribeOn(Schedulers.io());
return(flow);
}
}
It should be noted that, in a departure from the tutorial, the flowable.fromCallable() lambda returns false. In the tutorial, it returns true. I had assumed that a security check would fail if a false is returned, and that a failure would cause the hello service to fail to respond.
According to the tutorials, in ordeer to begin using the Security object, it is necessary to have a filter. The filter I created is shown below:
#Filter("/**")
public class HelloFilter implements HttpServerFilter
{
private final SecurityService secService;
public HelloFilter(SecurityService aSec)
{
System.out.println("Filter Created!");
secService = aSec;
}
#Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> theReq, ServerFilterChain theChain)
{
System.out.println("Filtering!");
Publisher<MutableHttpResponse<?>> resp = secService.checkAuthorization(theReq)
.doOnNext(res->{
System.out.println("Responding!");
});
return(resp);
}
}
The problem occurs when I run the microservice and access the Helo world URL. (http://localhost:8080/hello) I cannot cause the access to the service to fail. The filter catches all requests, and the security object is engaged, but it does not seem to prevent access to the hello service. I do not know what it takes to make the access fail.
Can someone help on this matter? Thank you.
You need to change request in your filter when you no have access to resource or process request as usual. Your HelloFilter looks like this:
#Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> theReq, ServerFilterChain theChain) {
System.out.println("Filtering!");
Publisher<MutableHttpResponse<?>> resp = secService.checkAuthorization(theReq)
.switchMap((authResult) -> { // authResult - is you result from SecurityService
if (!authResult) {
return Publishers.just(HttpResponse.status(HttpStatus.FORBIDDEN)); // reject request
} else {
return theChain.proceed(theReq); // process request as usual
}
})
.doOnNext(res -> {
System.out.println("Responding!");
});
return (resp);
}
And in the last - micronaut has security module with SecurityFilter, you can use #Secured annotations or write access rules in configuration files more examples in the doc
I'm trying to implement a WebFilter that checks a JWT and throw an exception if the check fails or the result is not valid. And I've a #ControllerAdvice class that handles those exceptions. But it doesn't work.
This is the WebFilter class:
#Component
public class OktaAccessTokenFilter implements WebFilter {
private JwtVerifier jwtVerifier;
#Autowired
public OktaAccessTokenFilter(JwtVerifier jwtVerifier) {
this.jwtVerifier = jwtVerifier;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return Optional.ofNullable(exchange.getRequest().getHeaders().get("Authorization"))
.flatMap(list -> list.stream().findFirst())
.filter(authHeader -> !authHeader.isEmpty() && authHeader.startsWith("Bearer "))
.map(authHeader -> authHeader.replaceFirst("^Bearer", ""))
.map(jwtString -> {
try {
jwtVerifier.decodeAccessToken(jwtString);
} catch (JoseException e) {
throw new DecodeAccessTokenException();
}
return chain.filter(exchange);
}).orElseThrow(() -> new AuthorizationException());
}
}
And the exception handler:
#ControllerAdvice
public class SecurityExceptionHandler {
#ExceptionHandler(AuthorizationException.class)
public ResponseEntity authorizationExceptionHandler(AuthorizationException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
#ExceptionHandler(DecodeAccessTokenException.class)
public ResponseEntity decodeAccessTokenExceptionHandler(DecodeAccessTokenException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
I think, the #ControllerAdvice class can not handle exceptions that WebFilter throws. Because, if I move the exceptions to the controller, it works.
I've change the code for this (for now):
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Optional<String> authJwt = Optional.ofNullable(exchange.getRequest().getHeaders().get("Authorization"))
.flatMap(list -> list.stream().findFirst())
.filter(authHeader -> !authHeader.isEmpty() && authHeader.startsWith("Bearer "))
.map(authHeader -> authHeader.replaceFirst("^Bearer", ""));
if (authJwt.isPresent()) {
String jwtString = authJwt.get();
try {
jwtVerifier.decodeAccessToken(jwtString);
} catch (JoseException e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().writeWith(Mono.empty());
}
} else {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().writeWith(Mono.empty());
}
return chain.filter(exchange);
}
What do you think about the problem? Do you know another way to implement it?
You might try defining an #Order() for both your #ControllerAdvice and #WebFilter beans, and giving #ControllerAdvice higher precedence.
However, I don't think that's the way to go, main reason being the #ControllerAdvice exception handlers don't return reactive types. Instead, I would define a bean which implements ErrorWebExceptionHandler instead. This handler is added to reactive flow by `spring-webflux, so you don't need to worry about the precedence. See this answer for details.
I had the same issue in webflux, you do not want to throw a direct exception or return a mono error in the webfilter), however you want to set the response to be the mono error containing the exception (Close to what you have done). This will allow the exception to be thrown in the correct part of the requests workflow and hence it will bubble up to your rest Controller advice
public class YourFilter implements WebFilter{
#Override
public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain){
return exchange.getResponse().writeWith(Mono.error(new YouException()));
}
}
I have tried that and I have a CORS error
Here is the error:
Access to XMLHttpRequest at 'http://localhost:8084/users/files' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.