I've created a service class to make an API call:
ServiceA {
public B sendReceive(Request req) {
return Optional.of(req).map(this::transform).map(apiService::call);
}
private ApiRequest transform(Request req) {
...
}
}
ApiService {
public B call(ApiRequest req) {
return webClient.post().bodyValue(req).bodyToMono().block();
}
#ControllerAdvice
ExceptionManager {
#ExceptionHandler(Exception.class)
public ResponseEntity handle(Exception ex) {
...
}
}
I tried to produce an exception in the transform method to test the ControllerAdvice class, but it does not work. The exception goes directly into InvocableHandlerMethod class instead of ExceptionManager class, which is inside the reactive package.
Since I'm not using any Mono/Flux, the typical .onError will not work for my case.
How do I make the ControllerAdvice working? Or how should I handle exception in the blocking call?
Related
I'm experimenting with the custom validator annotation with spring boot in my rest application.
here is the sample code from the validator, other requisite classes are present:
public class CustomerValidator implements ConstraintValidator<CustomerValidation, Customer>
{
public boolean isValid(Customer value, ConstraintValidatorContext cxt) {
if(value.getItemList.size() == 0) {
throw new CustomerOrderException();
}
if(value.getBalance() < value.getCost()) {
throw new InsufficientBalanceException();
}
return true;
}
}
I made a class annotated with controller advice and exception handler but it doesn't seem to intercept the exception thrown from the validator.
Ok I got it, I extended my controller advice class with ResponseEntityExceptionHandler and did the usual ExceptionHandler annotation specifying my custom exceptions.
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(value={CustomerOrderException.class})
protected ResponseEntity<Object> handleException(CustomerOrderException ex, WebRequest request) {
//return response entity
}
}
I am having problems testing my HandleException, even though I have searched I only find solutions for a standard controller that integrates error management but I have it separately.
The problem is that when it comes to doing the unit tests and its coverage, I don't know how to cover it properly.
#Order(Ordered.HIGHEST_PRECEDENCE)
#ControllerAdvice
public class RestExceptionHandler {
#ExceptionHandler(AmazonClientException.class)
protected ResponseEntity<Object> handleAmazonClientException(AmazonClientException ex) {
return buildResponseEntity(new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, ex));
}
#ExceptionHandler(AmazonS3Exception.class)
protected ResponseEntity<Object> handleAmazonS3Exception(AmazonS3Exception ex) {
return buildResponseEntity(new ApiError(HttpStatus.NOT_FOUND, ex));
}
private ResponseEntity<Object> buildResponseEntity(ApiError apiError) {
return new ResponseEntity<>(apiError, apiError.getStatus());
}
}
RestExceptionHandler.class
ApiError.class
#Data
public class ApiError {
private HttpStatus status;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
private LocalDateTime timestamp;
private String message;
private String debugMessage;
private ApiError() {
setTimestamp(LocalDateTime.now());
}
ApiError(HttpStatus status) {
this();
setStatus(status);
}
ApiError(HttpStatus status, Throwable ex) {
this();
setStatus(status);
setMessage("Unexpected error");
setDebugMessage(ex.getLocalizedMessage());
}
ApiError(HttpStatus status, String message, Throwable ex) {
this();
setStatus(status);
setMessage(message);
setDebugMessage(ex.getLocalizedMessage());
}
}
How can see, i only use my RestExceptionHandler for that, not call another Business class.
Any advice appreciated.
To unit test #ControllerAdvice annotated classes, you can use something called MockMvc which Spring provides.
It is a very elegant way of unit testing your controllers and subsequent controller advices.
So, let's say this is my controller
#RestController
#RequestMapping("api")
public class RobotController {
private RobotService robotService;
#GetMapping
public Collection<Integer> get() {
throw new DemoException();
}
}
And here's my controller advice; DemoException is just an empty class that extends RuntimeException.
#RestControllerAdvice
public class RestExceptionHandler {
#ExceptionHandler(DemoException.class)
public ResponseEntity<Object> handleException(DemoException dex) {
return ResponseEntity
.status(400)
.body("Bad request");
}
}
So, this means, sending a GET request to BASEURL/api will give me Bad request text response with 400 status code.
Okay, since we are done with the setup, let's move to the actual unit test. This is a very simple unit test, but it does what you need.
public class AdviceTest {
private final MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new RobotController())
.setControllerAdvice(new RestExceptionHandler())
.build();
#Test
void testGetFails() {
mockMvc.perform(
MockMvcRequestBuilders.get("/api")
).andExpect(
status().isBadRequest()
);
}
}
So, let me explain the initialization of the MockMvc object. There are two versions.
Standalone setup in which you just provide manually created controller objects.
Web application context in which you just provide an autowired application context.
You can also autowire MockMvc object.
Out of those 2, I really like the former, because it does not include the whole Spring context, hence it is faster.
There are a lot of methods available from checking the json content to paths. You can read more here:
Spring Guide
Baeldung
I am facing a problem using a REST service made up with Spring Boot 1.5. I am developing a REST service that acts as a proxy, forwarding requests to another REST service that exposes the same API.
#RestController
#RequestMapping("/user")
public class ProxyUserController {
// Some initialization code
#PostMapping
public ResponseEntity<?> add(#Valid #RequestBody User user) {
return restTemplate.postForEntity(userUrl, user, String.class);
}
#Configuration
public static class RestConfiguration {
#Bean
public RestTemplate restTemplate(UserErrorHandler errorHandler) {
return new RestTemplateBuilder().errorHandler(errorHandler).build();
}
}
#Component
public static class UserErrorHandler implements ResponseErrorHandler {
#Override
public boolean hasError(ClientHttpResponse response) {
return false;
}
#Override
public void handleError(ClientHttpResponse response) {
// Empty body
}
}
}
As you can see, to avoid that RestTemplate surrounds any error response with an exception that causes the creation of a new response with status 500, I defined a customer ResponseErrorHandler.
The problem I faced is that if the postForEntity returns a response with an HTTP Status different from 200, the response will never arrive at the caller, that hangs up until the timeout hits him.
However, if I create a new response starting from the one returned by the postForEntity, all starts to work smoothly.
#PostMapping
public ResponseEntity<?> add(#Valid #RequestBody User user) {
final ResponseEntity<?> response =
restTemplate.postForEntity(userUrl, user, String.class);
return ResponseEntity.status(response.getStatusCode()).body(response.getBody());
}
What the hell is going on? Why I can't reuse a ResponseEntity coming from another call?
Thanks to all.
now, I using the #ControllerAdvice in Spring 4.*.
using beforeBodyWrite method.
create custom annotation in controller class.
get the information of controller when #ControllerAdvice processing.
I want to know that request coming from what's controller class.
but, I don't know the solution.
any help.?
thanks
Although your question doesn't state clearly what is the purpose of what you are achieving and why do you need to create a custom annotation I'll post you some guidelines showing how to determine the source of your RuntimeException inside the ControllerAdvice
Given the following Rest controllers:
#RestController
public class CARestController {
#RequestMapping(value = "/test", method = RequestMethod.GET)
public String testException() {
throw new RuntimeException("This is the first exception");
}
}
#RestController
public class CAOtherRestController {
#RequestMapping(value = "/test-other", method = RequestMethod.GET)
public String testOtherException() {
throw new RuntimeException("This is the second exception");
}
}
Which both throw an exception, we can capture this exception by using the following ControllerAdvice and determine the origin of the exception using the stack trace.
#ControllerAdvice
public class CAControllerAdvice {
#ExceptionHandler(value = RuntimeException.class)
protected ResponseEntity<String> handleRestOfExceptions(RuntimeException ex) {
return ResponseEntity.badRequest().body(String.format("%s: %s", ex.getStackTrace()[0].getClassName(), ex.getMessage()));
}
}
This is how the output of the endpoints will look:
My recommendation is that instead of doing this, you declare your own set of exceptions and then capture them in your controller advice, with independence of where they were thrown:
public class MyExceptions extends RuntimeException {
}
public class WrongFieldException extends MyException {
}
public class NotFoundException extends MyException {
}
#ControllerAdvice
public class CAControllerAdvice {
#ExceptionHandler(value = NotFoundException .class)
public ResponseEntity<String> handleNotFound() {
/**...**/
}
#ExceptionHandler(value = WrongFieldException .class)
public ResponseEntity<String> handleWrongField() {
/**...**/
}
}
I'm trying to figure out the simplest way to take control over the 404 Not Found handler of a basic Spring Boot RESTful service such as the example provided by Spring:
https://spring.io/guides/gs/rest-service/
Rather than have it return the default Json output:
{
"timestamp":1432047177086,
"status":404,
"error":"Not Found",
"exception":"org.springframework.web.servlet.NoHandlerFoundException",
"message":"No handler found for GET /aaa, ..."
}
I'd like to provide my own Json output.
By taking control of the DispatcherServlet and using DispatcherServlet#setThrowExceptionIfNoHandlerFound(true), I was able to make it throw an exception in case of a 404 but I can't handle that exception through a #ExceptionHandler, like I would for a MissingServletRequestParameterException. Any idea why?
Or is there a better approach than having a NoHandlerFoundException thrown and handled?
It works perfectly Fine.
When you are using SpringBoot, it does not handle (404 Not Found) explicitly; it uses WebMvc error response. If your Spring Boot should handle that exception, then you should do some hack around Spring Boot. For 404, the exception class is NoHandlerFoundException; if you want to handle that exception in your #RestControllerAdvice class, you must add #EnableWebMvc annotation in your Application class and set setThrowExceptionIfNoHandlerFound(true); in DispatcherServlet. Please refer to the following code:
#SpringBootApplication
#EnableWebMvc
public class Application {
#Autowired
private DispatcherServlet servlet;
public static void main(String[] args) throws FileNotFoundException, IOException {
SpringApplication.run(Application.class, args);
}
#Bean
public CommandLineRunner getCommandLineRunner(ApplicationContext context) {
servlet.setThrowExceptionIfNoHandlerFound(true);
return args -> {};
}
}
After this you can handle NoHandlerException in your #RestControllerAdvice class
#RestControllerAdvice
public class AppException {
#ExceptionHandler(value={NoHandlerFoundException.class})
#ResponseStatus(code=HttpStatus.BAD_REQUEST)
public ApiError badRequest(Exception e, HttpServletRequest request, HttpServletResponse response) {
e.printStackTrace();
return new ApiError(400, HttpStatus.BAD_REQUEST.getReasonPhrase());
}
}
I have created ApiError class to return customized error response
public class ApiError {
private int code;
private String message;
public ApiError(int code, String message) {
this.code = code;
this.message = message;
}
public ApiError() {
}
//getter & setter methods...
}
According to the Spring documentation appendix A. there is a boolean property called spring.mvc.throw-exception-if-no-handler-found which can be used to enable throwing NoHandlerFoundException. Then you can create exception handler like any other.
#RestControllerAdvice
class MyExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(MyExceptionHandler.class);
#ResponseStatus(HttpStatus.NOT_FOUND)
#ExceptionHandler(NoHandlerFoundException.class)
public String handleNoHandlerFoundException(NoHandlerFoundException ex) {
log.error("404 situation detected.",ex);
return "Specified path not found on this server";
}
}
#ExceptionHandler itself without #ControllerAdvice (or #RestControllerAdvice) can't be used, because it's bound to its controller only.
In short, the NoHandlerFoundException is thrown from the Container, not from your application within your container. Therefore your Container has no way of knowing about your #ExceptionHandler as that is a Spring feature, not anything from the container.
What you want is a HandlerExceptionResolver. I had the very same issue as you, have a look at my solution over there: How to intercept "global" 404s on embedded Tomcats in spring-boot
The #EnableWebMvc based solution can work, but it might break Spring boot auto configurations.
The solution I am using is to implement ErrorController:
#RestController
#RequiredArgsConstructor
public class MyErrorController implements ErrorController {
private static final String ERROR_PATH = "/error";
#NonNull
private final ErrorAttributes errorAttributes;
#RequestMapping(value = ERROR_PATH)
Map<String, Object> handleError(WebRequest request) {
return errorAttributes.getErrorAttributes(request, false);
}
#Override
public String getErrorPath() {
return ERROR_PATH;
}
}
The solution for this problem is:
Configure DispatcherServlet to throw and exception if it doesn't find any handlers.
Provide your implementation for the exception that will be thrown from DispatcherServlet, for this case is the NoHandlerFoundException.
Thus, in order to configure DispatcherServlet you may use properties file or Java code.
Example for properties.yaml,
spring:
mvc:
throw-exception-if-no-handler-found: true
Example for properties.properties,
spring.mvn.throw-exception-if-no-handler-found=true
Example for Java code, we just want to run the command servlet.setThrowExceptionIfNoHandlerFound(true); on startup, I use the InitializingBean interface, you may use another way. I found a very well written guide to run logic on startup in spring from baeldung.
#Component
public class WebConfig implements InitializingBean {
#Autowired
private DispatcherServlet servlet;
#Override
public void afterPropertiesSet() throws Exception {
servlet.setThrowExceptionIfNoHandlerFound(true);
}
}
Be careful! Adding #EnableWebMvc disables autoconfiguration in Spring Boot 2, meaning that if you use the annotation #EnableWebMvc then you should use the Java code example, because the spring.mvc.* properties will not have any effect.
After configuring the DispatcherServlet, you should override the ResponseEntityExceptionHandler which is called when an Exception is thrown. We want to override the action when the NoHandlerFoundException is thrown, like the following example.
#ControllerAdvice
public class MyApiExceptionHandler extends ResponseEntityExceptionHandler {
#Override
public ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String responseBody = "{\"errormessage\":\"WHATEVER YOU LIKE\"}";
headers.add("Content-Type", "application/json;charset=utf-8");
return handleExceptionInternal(ex, responseBody, headers, HttpStatus.NOT_FOUND, request);
}
}
Finally, adding a break point to method handleException of ResponseEntityExceptionHandler might be helpful for debugging.