How to handle 404 exceptions using #ControllerAdvice and #ExceptionHandler? - java

I have a problem with Spring's exception handling for controllers. I have a class annotated with #RestControllerAdvice with a couple of #ExceptionHandler's, like this:
#ExceptionHandler(HttpRequestMethodNotSupportedException::class)
fun methodNotSupportedException(
exception: HttpRequestMethodNotSupportedException,
request: HttpServletRequest
): ResponseEntity<ApiError> {
logger().error("Method not supported: {}", exception.message)
val methodNotAllowed = HttpStatus.METHOD_NOT_ALLOWED
val apiError = logAndBuildApiError(request, methodNotAllowed, exception)
return ResponseEntity(apiError, methodNotAllowed)
}
and they work perfectly fine. In this case, when I'm trying to use an non-implemented HTTP method like POST:
{
"requestUri": "/api/v1/items",
"status": 405,
"statusText": "Method Not Allowed",
"createdAt": "2023-01-12T16:50:36.55422+02:00",
"errorMessage": "Request method 'POST' not supported"
}
What I would like to achieve is to handle situations when someone is trying to reach an non-existing endpoint, i.e. the correct one is GET http://localhost:8080/api/v1/items.
But when I'm trying to reach http://localhost:8080/api/v1/itemss, which is of course nonexistent, I recieve a regular Spring whitelabel error page, but I would like to receive a JSON like in the former example:
{
"requestUri": "/api/v1/itemss",
"status": 404,
"statusText": "Not Found",
"createdAt": "2023-01-12T16:52:06.932108+02:00",
"errorMessage": "Some error message"
}
How do I implement a #ExceptionHandler so it could handle exceptions related to non-existing resources?

spring.mvc.throw-exception-if-no-handler-found works in conjunction with
spring.mvc.static-path-pattern. By default, the static path pattern is /**, which includes the whitelabel error pages that you're seeing.
See https://github.com/spring-projects/spring-boot/pull/31660
and https://gitter.im/spring-projects/spring-boot?at=62ba1378568c2c30d30790af
and https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#web.servlet.spring-mvc.static-content
Option one is to set these two properties in your configuration.
spring:
mvc:
throw-exception-if-no-handler-found: true
static-path-pattern: /static
Option 2 is to add #EnableWebMvc to your spring boot application, and set the spring.mvc.throw-exception-if-no-handler-found property to true. By adding EnableWebMvc you'll be getting the WebMvcConfigurationSupport bean, which will cause Spring not to initialize the WebMvcAutoConfiguration and thereby not set the static-path-pattern.

Related

I'm Getting Inadequate Error Messages from Spring Boot

In my Spring Boot application, I specified my API using OpenApi 3.0.0. When I test its response to bad input, I'm not happy with some of the error messages. The messages are useful when Hibernate can't handle my input. They include the class, field, and even the illegal value. But when Spring Boot rejects my input without even entering my code, I just get the vague message The request cannot be fulfilled due to bad syntax. There's no information about what field is bad, or what object holds the bad field value.
When I specify my DTO in the .yaml file, two fields are required:
MenuItemOptionDto:
type: object
description: Option for a MenuItem
properties:
name:
type: string
deltaPrice:
type: number
description: Floating point price. Strings are easier to work with.
id:
type: integer
format: int32
required:
- name
- deltaPrice
But suppose I submit a DTO with a missing deltaPrice, like this: {"name": "onions"} The error message just says The request cannot be fulfilled due to bad syntax. I want the error message to say which DTO is incorrect, and which field is missing.
I have specified three relevant application properties. Any one of these will give me Hibernate validation error messages, but none give me spring-boot validation messages:
server.error.include-message=always
server.error.include-binding-errors=always
server.error.include-exception=true
And I've received advise to add a validator bean to my main application, which didn't help:
#ComponentScan(basePackages = {"com.myWork.dummy","org.openapitools",})
#EnableCaching
#SpringBootApplication
public class ServerMaster implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(ServerMaster.class);
public static void main(String[] args) {
new SpringApplication(ServerMaster.class).run(args);
}
#Override
public void run(String... arg0) { ... }
// This was suggested at https://stackoverflow.com/questions/49538896/spring-boot-error-message-doesnt-work
// in order to give me better error messages when OpenAPI validations are triggered, but it doesn't help.
#Bean public Validator validator() {
return new LocalValidatorFactoryBean();
}
}
When I generate the code, it doesn't matter if I turn on the performBeanValidation or useBeanValidation options. The generated code doesn't change. Either way, the #NotNull annotations are applied to the getters for the name and deltaPrice fields, and these are getting honored by the server, but without useful error messages.
Finally, I'm using Spring-Boot 2.3.4, and I declare a dependency on Spring Boot annotations:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Spring-Boot correctly rejects the input because the OpenAPI generator puts #NotNull annotations on the getters of the generated MenuItemOptionDTO, but since the code is generated, I can't customize them with an error message, and I don't want to turn off the generator. How can I get Spring or OpenAPI to give me better error messages?
Test Case
To see these messages in action, check out the code at https://github.com/SwingGuy1024/SpringBootDemo.22.05.25
The default SpringBoot error-handler does not provide a response body for MethodArgumentNotValidException:
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return this.handleExceptionInternal(ex, (Object)null, headers, status, request);
}
The good news: you can override this in your GlobalResponseExceptionHandler class:
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return handleExceptionInternal(ex, ex.getBindingResult(), headers, status, request);
}
In the above code, we simply return the entire binding-result as the response body. If you want to, you can tweak this (e.g. only include the errors).
When you call the controller with the invalid payload, you'll now get the following response:
{
"timestamp": "2022-05-28T19:40:47.295+00:00",
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"message": "Validation failed for object='menuItemOptionDto'. Error count: 1",
"errors": [
{
"codes": [
"NotNull.menuItemOptionDto.deltaPrice",
"NotNull.deltaPrice",
"NotNull.java.math.BigDecimal",
"NotNull"
],
"arguments": [
{
"codes": [
"menuItemOptionDto.deltaPrice",
"deltaPrice"
],
"arguments": null,
"defaultMessage": "deltaPrice",
"code": "deltaPrice"
}
],
"defaultMessage": "must not be null",
"objectName": "menuItemOptionDto",
"field": "deltaPrice",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
],
"path": "/demo/admin/menuItem/addOption/1"
}
Based on OpenAPI generator to spring-boot with custom java validations
You can add some another validation layer in your code, which is independent of OpenAPI generator. This layer will be called from
PetsController and PetsController will validate only basic OpenAPI
known constraints.
You can add you validations not via annotations, but via xml config as shown here.
maybe something else.
Hack it a bit. I was looking for a solution in which my custom validation will be defined in OpenAPI spec same way as “required”.
Naturally I decided not to use solutions 1 or 2 (even thought it
might be the right way for a lot of cases). I found out the
openapi-generator actually provides a way of modifying the way the
code is generated. That means that I can actually define custom
constraint in OpenAPI specs as my own made up properties.
Please follow instructions in the above link for implementing last method.

Logback does not log ConstraintViolationException in Spring Boot's #ExceptionHandler

I am doing some validation on a query parameter in my Spring Boot web service. In this case it is a parameter that does not match the regex [0-9]{3}. So in the service method, there is a validation:
#Pattern(regexp="[0-9]{3}") #Valid #RequestParam(value = "AngivelseFrekvensForholdUnderkontoArtKode", required = false) String angivelseFrekvensForholdUnderkontoArtKode
(angivelseFrekvensForholdUnderkontoArtKode is just the name of the query parameter)
I am working on a log manager that basically just prints log messages using logback and slf4j.
I have a writeInternalError(exception) in my log manager class which nicely logs an exception when told to:
public void writeInternalError(Exception exception) {
logger.error(exception.getClass().getName(), kv("LogType", exception), kv("LogMessage", exception));
}
except for when the ConstraintViolationException is caught by the #ExceptionHandler in my #ControllerAdvice. No errors are shown, and a Spring log is outputted instead of my expected custom log. When I debug, the logger.error() seems to be executed and no errors are shown.
I have made a quick fix method where I manually extract the information of the exception, but I want to use the same logging method for all exceptions:
public void writeTracelog(Exception exception) {
logger.error(exception.getClass().getName(), kv("LogType", "exception"), kv("ErrorMessage", exception.getMessage()), kv("StackTrace", exception.getStackTrace()));
}
The expected and unexpected logs I get are:
// The Spring log message shown instead of my custom error message:
{
"#timestamp": "2021-06-10T12:13:40.730+02:00",
"#version": "1",
"message": "Resolved [javax.validation.ConstraintViolationException: call29f0dab4A3094a30A1cdE29c01f28af8.angivelseFrekvensForholdUnderkontoArtKode: must match \"[0-9]{3}\"]",
"logger_name": "org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver",
"thread_name": "http-nio-8082-exec-1",
"level": "WARN",
"level_value": 30000
}
// How the log is supposed to look like
{
"#timestamp": "2021-06-10T14:35:18.257+02:00",
"#version": "1",
"message": "javax.validation.ConstraintViolationException",
"logger_name": "ClsLogManager",
"thread_name": "http-nio-8082-exec-1",
"level": "ERROR",
"level_value": 40000,
"LogType": "exception",
"LogMessage": {
"cause": null,
"stackTrace": [...],
"constraintViolations": null,
"message": "call29f0dab4A3094a30A1cdE29c01f28af8.angivelseFrekvensForholdUnderkontoArtKode: must match \"[0-9]",
"suppressed": [],
"localizedMessage": "call29f0dab4A3094a30A1cdE29c01f28af8.angivelseFrekvensForholdUnderkontoArtKode: must match \"[0-9]"
}
}
When I call writeInternalError() with any other exception, the log is nicely output. I have tried different ways of logging to see what works and what does not as you can see in the handler in the #ControllerAdvice
#ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handleConflict(ConstraintViolationException ex, HttpServletRequest request) {
...
// Get the invalid parameter from the ConstraintViolationException
if (invalidParameter.equalsIgnoreCase("angivelseFrekvensForholdUnderkontoArtKode")) {
errorMessage = setErrorMessage(request, "422.9", HttpStatus.UNPROCESSABLE_ENTITY.value(), invalidValue);
clsLogManager.writeTracelog(ex); // Outputs customized unwanted log
clsLogManager.writeInternalError(new ConstraintViolationException(null)); // Outputs exception in the format I want
clsLogManager.writeInternalError(ex); // Outputs nothing
responseEntity = writeToAuditlog(request, inputHeaders, errorMessage); // Outputs an info log as it supposed to
return responseEntity; // Outputs the ExceptionHandlerExceptionResolver message after the return
}
// Do something else in case of another error
}
}
It looks like the logger cannot handle the exception, but why doesn't it tell me why, in case that is true, and why is the ExceptionHandlerExceptionResolver doing it instead?
Update:
I looked into ExceptionHandlerExceptionResolver as saver suggested, and found out that the log comes from AbstractHandlerExceptionResolver's logException(). My custom logger class' method gets called before logException(), but it still doesn't print anything. Can it be because it is a ConstraintViolationException that contains the field constraintViolations and that the logger does not know how to handle this?
There is a setWarnLogCategory method that I guess I can switch off if I don't want the Spring log. I just can't find out how. The javadocs for logException in AbstractHandlerExceptionResolver indicate that there is a property for this, but I don't know how to set it.
Update:
The issue is in method subAppend(E event) in OutputStreamAppender that class is parent of ConsoleAppender and in line of encoding:
byte[] byteArray = this.encoder.encode(event);
Encoder tried serialize ConstraintViolationException exception and jackson fails with error: HV000116: getParameterIndex() may only be invoked for nodes of type ElementKind.PARAMETER.
And as result of encoding is empty byte array in case of exception and this is reason why nothing is logged in console. See below:
I don't have a quick fix for that right now.
Old proposal:
I recommend to debug method doResolveHandlerMethodException in ExceptionHandlerExceptionResolver class and you can see there is only one place when spring boot logger can log message with warning level and that logger will work only in case when something happened during invoking handler method for exception (For example: incorrect type of parameter in handler method and so on). You will see the reason why your handler method wasn't called.
Please pay attention to case when class ConstraintViolationException can be located in two different packages:
javax.validation.ConstraintViolationException
org.hibernate.exception.ConstraintViolationException
of course in our case we should use ConstraintViolationException from javax.validation package
Those exceptions are handled in one of the base methods of ResponseEntityExceptionHandler. You need to override it.

Customize Spring Boot error response code without changing the default body

Spring Boot by default returns a response body for exceptions that meets my needs out of the box:
{
"timestamp": 1587794161453,
"status": 500,
"error": "Internal Server Error",
"exception": "javax.persistence.EntityNotFoundException",
"message": "No resource exists for the given ID",
"path": "/my-resource/1"
}
However, I would like to customize the response code for different types of exceptions thrown by my application. Some of the exceptions are not ones I own, so I can't just stick a #ResponseStatus annotation on the exception class. I've tries using an #ExceptionHandler method with #ResponseStatus, but that is overwriting the response body, which I don't wish to happen. For instance, I would like to map javax.persistence.EntityNotFoundException to return status code 404.
#ExceptionHandler
#ResponseStatus(HttpStatus.NOT_FOUND)
public void handleEntityNotFoundException(EntityNotFoundException e) {
// This method returns an empty response body
}
This question is similar to mine, but it is also attempting to adjust the response body. I am hoping that there is a shorter, more idiomatic way of adjusting just the status code and not the body.
It turns out that this answer to the question I mentioned had the exact solution needed to solve this problem, even though it didn't quite fully the question asked. The trick was dropping the use of #ResponseStatus from the method, and manually setting the status on the HttpServletResponse using HttpServletResponse.sendError(). This serves the standard Spring Boot exception response, but with the updated status code.
#ExceptionHandler
public void handleEntityNotFoundException(EntityNotFoundException e, HttpServletResponse response) throws IOException {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
{
"timestamp": 1587794161453,
"status": 404,
"error": "Not Found",
"exception": "javax.persistence.EntityNotFoundException",
"message": "No resource exists for the given ID",
"path": "/my-resource/1"
}

Proper Handling of PropertyReferenceException for Query Params

In a Spring (Boot) application we're heavily using the Pageable interface and have our controller methods mostly defined like so:
public ResponseEntity<List<Thing>> fetchAll(Pageable pageable) {
Page things = this.thingService.findAll(pageable);
return new ResponseEntity<>(things.getContent(), HttpStatus.OK);
}
When accessing it via the following endpoint:
/api/things/?size=5&page=1&sort=name,asc
If the sort property, name in this case, is misspelled or doesn't exist, a PropertyReferenceException is thrown and a 500 sent back to the user with a message such as:
{
"timestamp": "2017-01-30 13:40:47",
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.data.mapping.PropertyReferenceException",
"message": "No property name found for type Thing!",
"path": "/api/things"
}
While this is technically the right error message, it doesn't seem like 500 is the appropriate code to respond with. 5xx level message are typically reserved for server issues and this is technically a user issue so more like a 4xx level message.
Best I can see if that maybe it's just a 400. My question is a) is 400 an appropriate response in this situation and b) is there an elegant way to handle this. My current solution is to catch that Exception in my ControllerAdvice class and just return the 400.

Spring Boot - Error Controller to handle either JSON or HTML

I have a spring boot application.
I have a custom error controller, that is mapped to using ErrorPage mappings. The mappings are largely based on HTTP Status codes, and normally just render a HTML view appropriately.
For example, my mapping:
#Configuration
class ErrorConfiguration implements EmbeddedServletContainerCustomizer {
#Override public void customize( ConfigurableEmbeddedServletContainer container ) {
container.addErrorPages( new ErrorPage( HttpStatus.NOT_FOUND, "/error/404.html" ) )
}
And my error controller:
#Controller
#RequestMapping
public class ErrorController {
#RequestMapping( value = "/error/404.html" )
#ResponseStatus(value = HttpStatus.NOT_FOUND)
public String pageNotFound( HttpServletRequest request ) {
"errors/404"
}
This works fine - If I just enter a random non-existent URL then it renders the 404 page.
Now, I want a section of my site, lets say /api/.. that is dedicated to my JSON api to serve the errors as JSON, so if I enter a random non-existent URL under /api/.. then it returns 404 JSON response.
Is there any standard/best way to do this? One idea I tried out was to have a #ControllerAdvice that specifically caught a class of custom API exceptions I had defined and returned JSON, and in my standard ErrorController checking the URL and throwing an apprpriate API exception if under that API URL space (but that didn't work, as the ExceptionHandler method could not be invoked because it was a different return type from the original controller method).
Is this something that has been solved?
The problem was my own fault. I was trying to work out why my #ExceptionHandler was not able to catch my exception and return JSON - As I suggested at the end of my question, I thought I was having problems because of conflicting return types - this was incorrect.
The error I was getting trying to have my exception handler return JSON was along the lines of:
"exception": "org.springframework.web.HttpMediaTypeNotAcceptableException",
"message": "Could not find acceptable representation"
I did some more digging/experimenting to try to narrow down the problem (thinking that the issue was because I was in the Spring error handling flow and in an ErrorController that was causing the problem), however the problem was just because of the content negotiation stuff Spring does.
Because my errorPage mapping in the web.xml was mapping to /error/404.html, Spring was using the suffix to resolve the appropriate view - so it then failed when I tried to return json.
I have been able to resolve the issue by changing my web.xml to /error/404 or by turning off the content negotiation suffix option.
Now, I want a section of my site, lets say /api/.. that is dedicated
to my JSON api to serve the errors as JSON, so if I enter a random
non-existent URL under /api/.. then it returns 404 JSON response.
Is there any standard/best way to do this? One idea I tried out was to
have a #ControllerAdvice that specifically caught a class of custom
API exceptions I had defined and returned JSON, and in my standard
ErrorController checking the URL and throwing an apprpriate API
exception if under that API URL space (but that didn't work, as the
ExceptionHandler method could not be invoked because it was a
different return type from the original controller method).
I think you need to rethink what you are trying to do here. According to HTTP response codes here
The 404 or Not Found error message is an HTTP standard response code
indicating that the client was able to communicate with a given
server, but the server could not find what was requested.
So when typing a random URL you may not want to throw 404 all the time. If you are trying to handle a bad request you can do something like this
#ControllerAdvice
public class GlobalExceptionHandlerController {
#ExceptionHandler(NoHandlerFoundException.class)
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
#ResponseBody
public ResponseEntity<ErrorResponse> noRequestHandlerFoundExceptionHandler(NoHandlerFoundException e) {
log.debug("noRequestHandlerFound: stacktrace={}", ExceptionUtils.getStackTrace(e));
String errorCode = "400 - Bad Request";
String errorMsg = "Requested URL doesn't exist";
return new ResponseEntity<>(new ErrorResponse(errorCode, errorMsg), HttpStatus.BAD_REQUEST);
}
}
Construct ResponseEntity that suites your need.

Categories

Resources