How to advise (AOP) spring webflux web handlers to catch and transform reactive error - spring

[UPDATE 2021-10-11] Added MCVE
https://github.com/SalathielGenese/issue-spring-webflux-reactive-error-advice
For reusability concerns, I run my validation on the service layer, which returns Mono.error( constraintViolationException )...
So that my web handlers merely forward the unmarshalled domain to the service layer.
So far, so great.
But how do I advise (AOP) my web handlers so that it returns HTTP 422 with the formatted constraint violations ?
WebExchangeBindException only handle exceptions thrown synchronously (I don't want synchronous validation to break the reactive flow).
My AOP advice trigger and error b/c :
my web handler return Mono<DataType>
but my advice return a ResponseEntity
And if I wrap my response entity (from the advice) into a Mono<ResponseEntity>, I an HTTP 200 OK with the response entity serialized :(
Code Excerpt
#Aspect
#Component
class CoreWebAspect {
#Pointcut("withinApiCorePackage() && #annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postMappingWebHandler() {
}
#Pointcut("within(project.package.prefix.*)")
public void withinApiCorePackage() {
}
#Around("postMappingWebHandler()")
public Object aroundWebHandler(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
try {
final var proceed = proceedingJoinPoint.proceed();
if (proceed instanceof Mono<?> mono) {
try {
return Mono.just(mono.toFuture().get());
} catch (ExecutionException exception) {
if (exception.getCause() instanceof ConstraintViolationException constraintViolationException) {
return Mono.just(getResponseEntity(constraintViolationException));
}
throw exception.getCause();
}
}
return proceed;
} catch (ConstraintViolationException constraintViolationException) {
return getResponseEntity(constraintViolationException);
}
}
private ResponseEntity<Set<Violation>> getResponseEntity(final ConstraintViolationException constraintViolationException) {
final var violations = constraintViolationException.getConstraintViolations().stream().map(violation -> new Violation(
stream(violation.getPropertyPath().spliterator(), false).map(Node::getName).collect(toList()),
violation.getMessageTemplate().replaceFirst("^\\{(.*)\\}$", "$1"))
).collect(Collectors.toSet());
return status(UNPROCESSABLE_ENTITY).body(violations);
}
#Getter
#AllArgsConstructor
private static class Violation {
private final List<String> path;
private final String template;
}
}

From observation (I haven't found any proof in the documentation), Mono.just() on response is automatically translated into 200 OK regardless of the content. For that reason, Mono.error() is needed. However, its constructors require Throwable so ResponseStatusException comes into play.
return Mono.error(new ResponseStatusException(UNPROCESSABLE_ENTITY));
Request:
curl -i --request POST --url http://localhost:8080/welcome \
--header 'Content-Type: application/json' \
--data '{}'
Response (formatted):
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Content-Length: 147
{
"error": "Unprocessable Entity",
"message": null,
"path": "/welcome",
"requestId": "7a3a464e-1",
"status": 422,
"timestamp": "2021-10-13T16:44:18.225+00:00"
}
Finally, 422 Unprocessable Entity is returned!
Sadly, the required List<Violation> as a body can be passed into ResponseStatusException only as a String reason which ends up with an ugly response:
return Mono.error(new ResponseStatusException(UNPROCESSABLE_ENTITY, violations.toString()));
Same request
Response (formatted):
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Content-Length: 300
{
"timestamp": "2021-10-13T16:55:30.927+00:00",
"path": "/welcome",
"status": 422,
"error": "Unprocessable Entity",
"message": "[IssueSpringWebfluxReactiveErrorAdviceApplication.AroundReactiveWebHandler.Violation(template={javax.validation.constraints.NotNull.message}, path=[name])]",
"requestId": "de92dcbd-1"
}
But there is a solution defining the ErrorAttributes bean and adding violations into the body. Start with a custom exception and don't forget to annotate it with #ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) to define the correct response status code:
#Getter
#RequiredArgsConstructor
#ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public class ViolationException extends RuntimeException {
private final List<Violation> violations;
}
Now define the ErrorAttributes bean, get the violations and add it into the body:
#Bean
public ErrorAttributes errorAttributes() {
return new DefaultErrorAttributes() {
#Override
public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(request, options);
Throwable error = getError(request);
if (error instanceof ViolationException) {
ViolationException violationException = (ViolationException) error;
errorAttributes.put("violations", violationException .getViolations());
}
return errorAttributes;
}
};
}
And finally, do this in your aspect:
return Mono.error(new ViolationException(violations));
And test it out:
Same request
Response (formatted):
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Content-Length: 238
{
"timestamp": "2021-10-13T17:07:07.668+00:00",
"path": "/welcome",
"status": 422,
"error": "Unprocessable Entity",
"message": "",
"requestId": "a80b54d9-1",
"violations": [
{
"template": "{javax.validation.constraints.NotNull.message}",
"path": [
"name"
]
}
]
}
The tests will pass. Don't forget some classes are newly from the reactive packages:
org.springframework.boot.web.reactive.error.ErrorAttributes
org.springframework.boot.web.reactive.error.DefaultErrorAttributes
org.springframework.web.reactive.function.server.ServerRequest

How about replacing the aspect with a #ControllerAdvice containing an #ExceptionHandler? But let us clean up the main application class, extracting the inner classes from it into an extra class:
package name.genese.salathiel.issuespringwebfluxreactiveerroradvice;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import static org.springframework.boot.SpringApplication.run;
#SpringBootApplication
#EnableAspectJAutoProxy
public class IssueSpringWebfluxReactiveErrorAdviceApplication {
public static void main(String[] args) {
run(IssueSpringWebfluxReactiveErrorAdviceApplication.class, args);
}
}
package name.genese.salathiel.issuespringwebfluxreactiveerroradvice;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import java.util.List;
import java.util.stream.Collectors;
import static java.util.stream.StreamSupport.stream;
#ControllerAdvice
public class ConstraintViolationExceptionHandler {
#ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<List<Violation>> handleException(ConstraintViolationException constraintViolationException) {
final List<Violation> violations = constraintViolationException.getConstraintViolations().stream()
.map(violation -> new Violation(
violation.getMessageTemplate(),
stream(violation.getPropertyPath().spliterator(), false)
.map(Path.Node::getName)
.collect(Collectors.toList())
)).collect(Collectors.toList());
return ResponseEntity.unprocessableEntity().body(violations);
}
#Getter
#RequiredArgsConstructor
static class Violation {
private final String template;
private final List<String> path;
}
}
Now your tests both pass.
BTW, not being a Spring user, I got the idea from this answer.

Related

Spring boot #Valid is not returning proper message

Controller Method
#PostMapping("/hello")
public Hello hello(#Valid #RequestBody Hello hello) {
return hello;
}
POJO
import jakarta.validation.constraints.NotBlank;
class Hello{
#NotBlank(message = "msg must be present")
String msg;
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
Upon hitting the above URL with the following payload
{
"msg":""
}
I am getting the following response.
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Invalid request content.",
"instance": "/hello"
}
It should ideally specify the message msg must be present.
What's wrong here?
The following things have been already tried
added server.error.include-message: always in application.properties file
#ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException e) {
return new ResponseEntity<Object>("ConstraintViolationException",
HttpStatus.BAD_REQUEST);
}
Thanks in advance 👏
Edit
I had a #RestControllerAdvice and it starts working fine, once i remove it. #RestControllerAdvice is needed in my case for the customization of exceptions.
You have to write a controller advice and return the interpolated message from the exception caught in handler for invalid method argument.
#RestControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
String bodyOfResponse = ex.getBindingResult().getFieldErrors().get(0).getDefaultMessage();
return new ResponseEntity(bodyOfResponse, HttpStatus.BAD_REQUEST);
}
}
And after that you should get back message msg must be present along with 400. In case you have more constraints, you should iterate over field errors, and get(0) is only for demonstration purpose.

How to log json response in Spring Boot?

If I define a response entity class and return it by my rest controller, controller will change this class to json string. I want to log this json string, how to do it? Thanks.
For example:
Response entity class:
#Data
#AllArgsConstructor
public class ResponseEntity {
String code;
String message;
}
Rest Controller:
#RestController
#RequestMapping("/")
public class StudentController {
#RequestMapping("test")
public ResponseEntity test(){
return new ResponseEntity(200,"success");
}
}
I want to record this json response in log
{"code":200,"message":"success"}
The simplest and handy way is convert it to JSON string manually and then log it. You can convert it to JSON by ObjectMapper.
#Log4j2
#RequestMapping("/")
public class StudentController {
#Autowired
private ObjectMapper objectMapper;
#RequestMapping("test")
public ResponseEntity test() throws Exception {
ResponseEntity entity = new ResponseEntity(200, "success");
log.debug("{}", objectMapper.writeValueAsString(entity));
return entity;
}
}
Or you can use Logstash json_event pattern for log4j if you need advanced feature.
In order to accomplish you desired behaviour the spring framework offers an utility class which does exactly the requested job for your.
Just declare this:
#Configuration
public class RequestLoggingFilterConfig {
#Bean
public CommonsRequestLoggingFilter logFilter() {
CommonsRequestLoggingFilter filter
= new CommonsRequestLoggingFilter();
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setMaxPayloadLength(10000);
filter.setIncludeHeaders(false);
filter.setAfterMessagePrefix("REQUEST DATA : ");
return filter;
}
}
And add to your logging file this appender:
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter">
<level value="DEBUG" />
</logger>
Same result can be done with this application.properties property
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
this should be enough, but, in case you want a fine grained control on the logging operation you can create your own filter by extending AbstractRequestLoggingFilter .
add this in responseHandler class:--
package net.guides.springboot.springbootcrudrestapivalidation.response;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
public class ResponseHandler {
public static ResponseEntity<Object> generateResponse(String message, HttpStatus status, Object responseObj) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("message", message);
map.put("status", status.value());
map.put("data", responseObj);
return new ResponseEntity<Object>(map,status);
}
}
and then this in controller class:_-
#PostMapping( "employees")
public ResponseEntity<Object> Post(#Valid #RequestBody Employee employee) {
employeeRepository.save(employee);
try {
Employee emp = EmployeeRepository.Post(employee);
return ResponseHandler.generateResponse("Successfully added data!", HttpStatus.OK, emp);
} catch (Exception e) {
return ResponseHandler.generateResponse(e.getMessage(), HttpStatus.MULTI_STATUS,null);
}
output
{
"data": {
"id": 1,
"dob": "1994-10-17",
"doj": "2018-10-17",
"gender": "male",
"emailId": "abhih#gmail.com",
"age": "27",
"phoneNumber": "+918844945836",
"address": "Dhan",
"empName": "akash"
},
"message": "Successfully added data!",
"status": 200
}

Interceptor and global exception handling

I have a post-interceptor. When the control layer is executed and returns information, the post-interceptor will be executed. At this time, an exception in the post-interceptor will be caught by the global exception handling and a prompt message will be returned. Use "postman" to test and control The information of layer and global exception handling is returned at the same time. Is this really returned? I wrote a test example. In the same situation, only the information of the control layer is returned. I think it should return the information of global exception handling.
Controller
#RestController
#RequestMapping("/v1/book")
#Validated
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
#GetMapping("/search")
public R searchBook(#RequestParam(value = "q", required = false, defaultValue = "") String q) {
return R.select(bookService.getBookByKeyword(q));
}
}
Interceptor
public class LogInterceptor extends HandlerInterceptorAdapter {
public LogInterceptor(LoggerResolver loggerResolver) {
this.loggerResolver = loggerResolver;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
// There will be a runtime exception here
}
}
Global Exception Handing
#Order
#RestControllerAdvice
#Slf4j
public class RestExceptionHandler {
/**
* Exception
*/
#ExceptionHandler({Exception.class})
public R processException(Exception exception) {
log.error("", exception);
return R.error();
}
}
Result
{
"code": 200,
"data": [
// ...
],
"type": "success",
"message": "OK"
}{
"code": 500,
"type": "error",
"message": "Internal Server Error"
}
"R extends HashMap<String, Object>", used to unify the return structure.
looking at your code snippet, I'm not sure what are those R in the searchBook and processException
try this (edit the processException to meet your specs):
#GetMapping("/search")
public ResponseEntity<?> searchBook(#RequestParam(value = "q", required = false, defaultValue = "") String q) {
return new ResponseEntity<>(bookService.getBookByKeyword(q), HttpStatus.OK);
}
#ExceptionHandler({Exception.class})
public ResponseEntity<?> processException(Exception exception) {
return new ResponseEntity<>(new ErrorDTO(exception.getMessage()), HttpStatus.UNPROCESSABLE_ENTITY);
}

Spring Boot 2.2.5 404 page not found custom json response

How can I have a custom json for my 404 pages ?
actually what I need is to be able to create custom json errors for my application.
for example for 404,401,403,422, ...
I searched a lot and what I found is :
package ir.darsineh.lms.http.exceptionHandler;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
#ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(NoHandlerFoundException.class)
public void springHandleNotFound(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.NOT_FOUND.value());
}
}
and here is the error I get :
Ambiguous #ExceptionHandler method mapped for [class org.springframework.web.servlet.NoHandlerFoundException]
I need my api response body json to be something like this :
{"code": 404, "message": "page not found"}
First, you should let Spring MVC to throw exception if no handler is found:
spring.mvc.throw-exception-if-no-handler-found=true
Then, the exception must be caught using a #ControllerAdvice:
#ControllerAdvice
public class CustomAdvice {
// 404
#ExceptionHandler({ NoHandlerFoundException.class })
#ResponseBody
#ResponseStatus(HttpStatus.NOT_FOUND)
public CustomResponse notFound(final NoHandlerFoundException ex) {
return new CustomResponse(HttpStatus.NOT_FOUND.value(), "page not found");
}
}
#Data
#AllArgsConstructor
class CustomResponse {
int code;
String message;
}
Do not forget to add #EnableWebMvc annotation to your app.
ResponseEntityExceptionHandler class already has handleNoHandlerFoundException() method defined as below.
protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return this.handleExceptionInternal(ex, (Object)null, headers, status, request);
}
Since the method signatures (parent class and our implementation class) are different, it resulted in ambiguous error. Using the same signature will override the above method with our custom implementation.
#ExceptionHandler(NoHandlerFoundException.class)
protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
ErrorResponse error = new ErrorResponse("404", "page not found");
return new ResponseEntity(error, HttpStatus.NOT_FOUND);
}
Hope this helps!!

How to show Exception name in Spring Boot's JSON Response

I have a Spring Controller which may throw a Runtime Exception at a certain point:
#RequestMapping("/list")
public List<User> findAll() {
// if here
throw new RuntimeException("Some Exception Occured");
}
When I request that URI, the JSON does not include the Exception name ("Runtime Exception"):
curl -s http://localhost:8080/list
{
"timestamp": "2020-04-01T13:15:11.091+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Some Exception Occured",
"path": "/list"
}
Is there way to have it included in the JSON which is returned?
Thanks!
I think you can do that by extending the DefaultErrorAttributes and provide a custom list of attributes to be displayed in the returned JSON. For example, the following one provides a custom error response for Field errors:
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.context.MessageSource;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.context.request.WebRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class ResolvedErrorAttributes extends DefaultErrorAttributes {
private MessageSource messageSource;
public ResolvedErrorAttributes(MessageSource messageSource) {
this.messageSource = messageSource;
}
#Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
resolveBindingErrors(errorAttributes);
return errorAttributes;
}
private void resolveBindingErrors(Map<String, Object> errorAttributes) {
List<ObjectError> errors = (List<ObjectError>) errorAttributes.get("errors");
if (errors == null) {
return;
}
List<String> errorMessages = new ArrayList<>();
for (ObjectError error : errors) {
String resolved = messageSource.getMessage(error, Locale.US);
if (error instanceof FieldError) {
FieldError fieldError = (FieldError) error;
errorMessages.add(fieldError.getField() + " " + resolved + " but value was " + fieldError.getRejectedValue());
} else {
errorMessages.add(resolved);
}
}
errorAttributes.put("errors", errorMessages);
}
}
In this tutorial you can find some more details about Spring Boot custom error responses.

Resources