Spring Boot, AOP and exception handler - java

Context
What I'm satisfied with
I'm creating a Spring Boot REST service returning a ResponseEntity<StreamingResponseBody>. I also have a #ControllerAdvice for exception handling. It all works fine.
I've been asked to provide logging (endpoints being called, execution time, HTTP status and such). The most solid way I can think of to do it was to use an Aspect. I made one with some #Around pointcuts, like below, and that, too, works fine in the nominal case.
#Around(value = "#annotation(requestMapping)", argNames = "pjp,requestMapping")
public Object trackExecutionTime(ProceedingJoinPoint pjp, RequestMapping requestMapping) throws Throwable {
return trackExecutionTime(pjp, requestMapping.method()[0], requestMapping.value()[0]);
}
What's brittle or broken
My problem now occurs in case of an exception. I couldn't find a better way than injecting my exception handler into my Aspect. It's dirty, brittle, and even completely broken in my case:
// Simplified
private Object trackExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object result = null;
try {
result = pjp.proceed();
} catch (MyException1 e) {
result = exceptionHandler.handleMyException1(e);
} catch (Exception e) {
result = exceptionHandler.handleException(e, webRequest);
} finally {
stopWatch.stop();
doLog(result, stopWatch.getTotalTimeMillis(),
((ResponseEntity) result).getStatus());
}
return result;
}
Brittle: new cases in exception handler means new catches to add here. I don't like it.
Broken: Spring isn't happy, at all. He expects a StreamingResponseBody and I return an ErrorDto. The ExceptionHandling is broken and the endpoint returns Spring's generic error + 500 instead of my DTO + appropriate HTTP status.
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalArgumentException: StreamingResponseBody expected: lu.pgd.dri.bidas.nist.rest.dto.NistRestApiErrorDto
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.24.jar:5.3.24]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:517) ~[jakarta.servlet-api-4.0.4.jar:4.0.4]
...
Caused by: java.lang.IllegalArgumentException: StreamingResponseBody expected: lu.pgd.dri.bidas.nist.rest.dto.NistRestApiErrorDto
at org.springframework.util.Assert.instanceCheckFailed(Assert.java:702) ~[spring-core-5.3.24.jar:5.3.24]
at org.springframework.util.Assert.isInstanceOf(Assert.java:602) ~[spring-core-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBodyReturnValueHandler.handleReturnValue(StreamingResponseBodyReturnValueHandler.java:90) ~[spring-webmvc-5.3.24.jar:5.3.24]
...
I of course did a PoC before starting this aspect, but I used ResponseEntity<String> in both success and failure cases, so I missed this issue.
My questions
Real, primary question: Is there a way to enhance my aspect so that I can achieve my purpose?
Bonus question: Does that enhancement allow me to handle exceptions in a generic way, by throwing it to some kind of Dispatcher in the Spring context, for instance?
As a potential lead, I considered not catching the exception and making the ExceptionHandler call the log service itself, but then I don't know how to get the StopWatch to have the execution time.
Thanks for any insight!

Related

Stacktrace of custom exception is not printed in Spring Boot 2.3

Error stacktrace is not printed in console for the custom exception that is annotated with #ResponseStatus
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class InternalErrorException extends RuntimeException {
public InternalErrorException(String message) {
super(message);
}
public InternalErrorException(String message, Throwable throwable) {
super(message, throwable);
}
}
Throwing exception like throw new InternalErrorException("error", e), never get the stacktrace printed in the console unlesss I remove the annotation #ResponseStatus
How could I get it printed while keeping the annotation #ResponseStatus?
See Annotation Type ResponseStatus API doc.
Warning: when using this annotation on an exception class, or when setting the reason attribute of this annotation, the HttpServletResponse.sendError method will be used.
With HttpServletResponse.sendError, the response is considered complete and should not be written to any further. Furthermore, the Servlet container will typically write an HTML error page therefore making the use of a reason unsuitable for REST APIs. For such cases it is preferable to use a ResponseEntity as a return type and avoid the use of #ResponseStatus altogether.
HttpServletResponse.sendError does not throw your error and I guess it is never logged because of that.
Maybe you want to implement exception handler for that exception to get it logged.
Related question

Spring ExceptionHandler but for normal beans

I have been able to successfully use #ExceptionHandler annonated methodsorg.springframework.web.bind.annotation.ExceptionHandler in Controller Classes in my Spring projects to handle exceptions thrown by spring #RestController
Working example:
#Validated
#RestController
#RequestMapping(value = UrlsProperties.API_PATH, produces = MediaType.APPLICATION_JSON, consumes = { MediaType.APPLICATION_JSON })
#Api(value = "MyController", description = "MyController processing and forwarding controller")
public class MyController {
private static Logger log = LogManager.getLogger(MyController.class);
...
#JsonFormat
#ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseMessage handleMissingParams(MissingServletRequestParameterException ex) {
String name = ex.getParameterName();
log.error(name + " parameter is missing");
return new ResponseMessage(400, ex.getMessage());
}
}
I am trying to achieve the same way of exception handling but for a normal bean, [ not a controller ]. Simply adding an #ExceptionHanlder annotated method did not seem to catch the exceptions thrown by that bean's methods.
My question is how to handle exceptions thrown by a bean by writing a method inside this bean?
#ExceptionHandler annotation is not for general exception handling. It's used in controllers to convert an exception into a proper HTTP response. It won't work for normal beans, because only controllers return a response.
If any code (doesn't need to be in a bean) throws an exception and you don't handle it, it would eventually propagate up to your controller's exception handler and it would be converted to a response. That would be poor design though, as you should handle exceptions as early as you can.
What you can do is create exceptions that are meant to be propagated to your exception handlers. Your code catches an exception, then re-throws it wrapped into your own exception (such as IllegalRequestException). The handler then returns an error code and details to the caller.

How could we use #ExceptionHandler with spring web flux?

In spring web we could use annotation #ExceptionHandler for handling server and client errors for controllers.
I've tried to use this annotation with web-flux controller and it still worked for me, but after some investigation I've found out here
The situation with Spring Web Reactive is more complicated. Because
the reactive streams are evaluted by a different thread than the one
that executes the controllers method, the exceptions won’t be
propagated to the controller thread automatically. This means that the
#ExceptionHandler method will work only for exceptions that are thrown
in the thread that handles the request directly. Exceptions thrown in
the stream will have to be propagated back to the thread if we want to
use the #ExceptionHandler feature. This seems like a bit of a let down
but at the time of writing this Spring 5 is still not released so
error handling might still get better.
So my question is how to propagate back exception to the thread. Is there a good example or article about using #ExceptionHandler and Spring web flux?
Updated:
From spring.io it looks like it's supported, but still lack general understanding
Thanks,
Now it is possible to use the #ExceptionHandler as well as #RestControllerAdvice or even #ControllerAdvice in Spring WebFlux.
Example:
Add the webflux dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Create your class ExceptionHandler
#RestControllerAdvice
public class ExceptionHandlers {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionHandlers.class);
#ExceptionHandler(Exception.class)
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String serverExceptionHandler(Exception ex) {
LOGGER.error(ex.getMessage(), ex);
return ex.getMessage();
}
}
Create a Controller
#GetMapping(value = "/error", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Mono<String> exceptionReturn() {
return Mono.error(new RuntimeException("test error"));
}
Example extracted here:
https://ddcode.net/2019/06/21/spring-5-webflux-exception-handling/
You can use #ExceptionHandler annotated methods to handle errors that happen within the execution of a WebFlux handler (e.g., your controller method). With MVC you can indeed also handle errors happening during the mapping phase, but this is not the case with WebFlux.
Back to your exception propagation question, the article you're sharing is not accurate.
In reactive applications, the request processing can indeed hop from one thread to another at any time, so you can't rely on the "one thread per request" model anymore (think: ThreadLocal).
You don't have to think about exception propagation or how threads are managed, really. For example, the following samples should be equivalent:
#GetMapping("/test")
public Mono<User> showUser() {
throw new IllegalStateException("error message!");
}
#GetMapping("/test")
public Mono<User> showUser() {
return Mono.error(new IllegalStateException("error message!"));
}
Reactor will send those Exceptions as error signals as expected in the Reactive Streams contract (see the "error handling" documentation section for more on that).
not an exact answer to the original question, but 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.
{
"timestamp": 1529138182607,
"path": "/api/notes/f7b.491bc-5c86-4fe6-9ad7-111",
"status": 400,
"error": "Bad Request",
"message": "For input string: \"f7b.491bc\""
}
The following global error handler did the trick for me:
import org.springframework.web.server.ResponseStatusException;
#Slf4j
#RestControllerAdvice
#ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class MyCustomReactiveErrorHandling {
#ExceptionHandler(MyCustomNotFoundException.class)
public void handleMyCustomException(MyCustomNotFoundException ex) {
throw new ResponseStatusException(404, "Data not found!", ex);
}
}
Throwing my exceptions returns the correct http status code at the rest service.

Spring Boot handle SizeLimitExceededException

I use Spring boot 1.5.7.
I have not configured CommonsMultipartResolver, because Spring Boot handles file uploads already.
If my upload exceeds the maximum permited size, an ugly Exception is thrown.
This is handled by my controller.
#ControllerAdvice
public abstract class DefaultController implements InitializingBean {
#ExceptionHandler(Exception.class)
public ResponseEntity<ServiceException> handleException(final Exception ex) {
...
} else if (ex instanceof MultipartException) {
MultipartException me = (MultipartException) ex;
Throwable cause = ex.getCause();
if (cause instanceof IllegalStateException) {
Throwable cause2 = cause.getCause();
if (cause2 instanceof SizeLimitExceededException) {
// this is tomcat specific
SizeLimitExceededException slee = (SizeLimitExceededException) cause2;
}
}
}
This kind of handling is not only complex but also sadly Tomcat specific, because the SizeLimitExceededException is in the package org.apache.tomcat.util.http.fileupload.FileUploadBase.
How can I handle the error case, that some one uploads a bigger file then allowed and return a nice message, regardless which Servlet Engine is used?
You could define a exception handler method in your #ControllerAdvice which is specifically for MultipartException and then qualify it with a specific HttpStatus. For example:
#ExceptionHandler(MultipartException.class)
#ResponseStatus(value = HttpStatus.PAYLOAD_TOO_LARGE)
public ResponseEntity<ServiceException> handleMultipartException(MultipartException ex) {
...
}
This should allow you to focus on the 'maximum file size' exception without having to get into servlet container specifics.
Update 1 in response to this comment:
Sounds good, what about getPermittedSize and getActualSize provided by SizeLimitExceededException is there a chance to get this values not only if Tomcat is used?
By intercepting this error on the basis of (a) the exception type and (b) the HTTP status ... you are making the solution generally applicable. But in so doing you might lose the sort of detailed information that one servlet container (but perhaps not another) might give you. You could perhaps enforce your own maximum size by setting spring.http.multipart.max-file-size in which case you would be able to report 'permitted size' but if you want to report 'actual size' then you'll have to consider one of the following:
Have to use something provided by the servlet container
Choose a spring.http.multipart.max-file-size which is less thatn the supported maximum for your servlet container and then apply your own max size check inside your controller and throw your own specific exception type containing the actual and permitted sizes.
This working fine in my #RestControllerAdvice and the exception is more specific than MultipartException :
#ExceptionHandler(MaxUploadSizeExceededException.class)
#ResponseStatus(value = HttpStatus.PAYLOAD_TOO_LARGE)
public ResponseEntity handleMultipartException(MaxUploadSizeExceededException e) {
Map<String, String> result = new HashMap<>();
result.put("message", "Error ==> Large File ");
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body(result);
}
PS : when using #RestControllerAdvice(annotations = RestController.class)
it doens't work .. you have to put #RestControllerAdvice without RestController.class annotation

Spring 3.2 DeferredResult - How to set status code for error response?

Spring Web 3.2 comes with a DeferredResult class for asynchronous request processing. It has a setErrorResult for providing an alternative response if something goes wrong, but no option to supply a http error code.
Surely it must be possible to control the http response code for failed requests.. How do I do that using the new Spring api?
The doc for setErrorResult method says the following:
Set an error value for the DeferredResult and handle it. The value may
be an Exception or Throwable in which case it will be processed as if
a handler raised the exception.
I suppose by setting an Exception, you may trigger an exception handler that returns the code you desire.
deferredResult.setErrorResult(new Exception());
This will always set the HTTP response code to 500. For finer control HttpServletResponse.setStatus seems to work.
This will work with user411180's client side.
public DeferredResult<List<Point>> getMessages(#RequestParam int reqestedIndex,
final HttpServletResponse response) {
final DeferredResult<List<Point>> deferredResult = new DeferredResult<>();
deferredResult.onCompletion(...);
deferredResult.onTimeout(new Runnable() {
#Override
public void run() {
deferredResult.setErrorResult("Explanation goes here.");
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); //or SC_NO_CONTENT
}
});
longPollRequests.put(deferredResult, reqestedIndex);
return deferredResult;
}
The exception that you pass as the argument to setErrorResult can be
annotated with #ResponseStatus. e.g. create an exception class of your own:
#ResponseStatus(HttpStatus.NOT_FOUND)
class NotFoundException extends RuntimeException {
// add your own constructors to set the error message
// and/or cause. See RuntimeException for valid ctors
}
Then in your code use it with the constructor you have created, for example:
deferredResult.setErrorResult(new NotFoundException(reason, cause));

Categories

Resources