How to get the status code of an exception from a ControllerAdvice in Spring Boo? - spring-boot

I'm using Spring Boot 3.x and I have a controller advice that looks like this:
#ControllerAdvice
class GlobalExceptionHandler {
#ExceptionHandler(Exception::class)
fun handleErrors(ex: Exception): ResponseEntity<ProblemDetail> {
val httpStatusCode = HttpStatusCode.valueOf(500)
val problemDetail = ProblemDetail.forStatusAndDetail(httpStatusCode, "error")
return ResponseEntity(problemDetail, httpStatusCode)
}
}
But if I get an exception, such as an org.springframework.security.access.AccessDeniedException, I want to return 403 instead of 500.
I know I can create a new method like this to return 403:
#ControllerAdvice
class GlobalExceptionHandler {
#ExceptionHandler(AccessDeniedException::class)
fun handleAccessDeniedErrors(ex: AccessDeniedException): ResponseEntity<ProblemDetail> {
val httpStatusCode = HttpStatusCode.valueOf(403)
val problemDetail = ProblemDetail.forStatusAndDetail(httpStatusCode, "error")
return ResponseEntity(problemDetail, httpStatusCode)
}
#ExceptionHandler(Exception::class)
fun handleErrors(ex: Exception): ResponseEntity<ProblemDetail> {
val httpStatusCode = HttpStatusCode.valueOf(500)
val problemDetail = ProblemDetail.forStatusAndDetail(httpStatusCode, "error")
return ResponseEntity(problemDetail, httpStatusCode)
}
}
But as you can see, the only thing that is different between the two methods is the status code. So my question is, how can I modify the original handleErros method to not only return 500 but rather return the status code that is expected for the exception by default (in the case of AccessDeniedException, that would be 403)?
I.e. I'm looking for the same status code that Spring Boot returns by default if I hadn't added the GlobalExceptionHandler bean. I'm not looking for something like manually mapping each exception type to a status code (using something like instanceof).

Related

Spring ControllerAdvice global exception handler not returning response

I have a Groovy Spring Boot microservice that returns a list of posts. Requests come into the controller, the controller invokes a method in a service class, and if no posts are found, a custom error message is thrown.
I've created a controller annotated with #ControllerAdvice that I want to intercept errors, and a handler specific to the custom error. It should return a POJO. At present the ControllerAdvice handler is being invoked, but the response from the microservice is a 500 error
The controller
#RequestMapping(value ='getCommentList', method = RequestMethod.POST)
def getCommentList(#RequestBody requestParams) {
def response
String userId = requestParams.userId
String uuid = requestParams.uuid
response = commentService.findCommentsByUserIdAndUuid(userId, uuid)
}
The service method
def findCommentsByUserIdAndUuid(String userId, String uuid) {
User user = userRepository.findByUserId(userId)
Long userId = tenant?.id
List responses = commentRepository.findCommentsByUserAndUuidNotDeleted(userId, uuid)
if (responses.size() == 0) {
throw new CommentsNotFoundException()
} else {
def data = JsonOutput.toJson(retMap)
return data
}
}
the custom exception class
package com.news.exception
class CommentsNotFoundException extends Exception {
String errorMessage
def dataReturned
public commentsNotFoundException() {
NewsFeedMessage.buildRequestErrorMessageWithData(errorMessage, dataReturned)
}
}
The POGO to be returned
package com.news.utils
import org.springframework.http.HttpStatus
class NewsFeedMessage {
int httpStatus
String type
String message
def dataReturned
def subMessages = []
static NewsFeedMessage buildRequestErrorMessageWithData(mainMessage, data) {
return new NewsFeedMessage(
httpStatus: 400,
message: mainMessage,
dataReturned: data
)
}
}
And the global exception handler with#ControllerAdvice annotation
package com.news.exception
import com.newa.utils.NewsFeedMessage
import org.springframework.boot.configurationprocessor.json.JSON
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(CommentsNotFoundException.class)
public NewsFeedMessage handleException() {
def response = NewsFeedMessage.buildRequestErrorMessageWithData("testing 1234", [])
//return response as JSON
}
}
When debugging, The code gets as far as
def response = NewsFeedMessage.buildRequestErrorMessageWithData("testing 1234", [])
However nothing is returned.
The returned message using Postman is:
{
"timestamp": "2020-10-23T16:27:35.572+0000",
"status": 500,
"error": "Internal Server Error",
"message": "No message available",
"path": "/comment/getCommentList"
}
Is there anything here that is glaringly wrong?
I think NewsFeedMessage is not supported return type. Check ExceptionHandler docs and there is a block about supported return types. Try returning a ResponseEntity<>(new NewsFeedMessage()).

Custom HTTP status Code (in 900 Series) on RestController Advice

My application is supposed to return a custom HTTP status code when there is a business exception.
I'm trying to implement the HTTP status as 999 in case of a business exception but could not find a way to implement it in rest controller advice.
I want to return HTTP status 999 in case of a business exception. Please guide me on how to implement it.
#Import({ ErrorCodeUtil.class })
#Slf4j
#RestControllerAdvice
public class GlobalExceptionHandler {
#Autowired
private ErrorCodeUtil errorCodeUtil;
#ExceptionHandler(BusinessException.class)
#ResponseStatus(// Return 999 here)
#ResponseBody
public ErrorResult handleException(BusinessException e) {
String msg = e.getMessage();
log.error(msg, e);
return new ErrorResult(e.getErrorCode(), msg);
}
According to the standard, there are only five classes of Http Status code from 1xx to 5xx. Check this link.
What you can do is in your controller advice, define the return type with status code property.
For example:
ErrorResult errorResult = new ErrorResult();
errorResult.setStatusCode(900);
errorResult.setMessage(e.getMessage());
You can use #ResponseStatus(code = HttpStatus.BAD_GATEWAY)
instead. Now, the method looks like:
#ExceptionHandler(BusinessException.class)
#ResponseStatus(code = HttpStatus.BAD_GATEWAY)
#ResponseBody
public ErrorResult handleException(BusinessException e) {
ErrorResult errorResult = new ErrorResult();
errorResult.setStatusCode(e.getStatusCode()); //set status code 900 when throwing exception or use errorResult.setStatusCode(900);
errorResult.setMessage(e.getMessage());
}

upgrading spring boot with groovy controller returns 406 causing HttpMediaTypeNotAcceptableException

I have a Groovy application that I am dealing with which is having some odd behavior when upgrading from spring-boot 1.3.0.RELEASE to 1.4.0.RELEASE. The controller always returns a 406 on any error and I am not sure what type of content it expects to return. The code is below:
SomeController.groovy:
#RestController
#RequestMapping('/some/mapping')
class SomeController extends AbstractController {
#Autowired
private SomeService someService
#RequestMapping(path = '/abc/{some_param}/some_action', method = RequestMethod.PUT, consumes = MediaType.TEXT_PLAIN_VALUE)
#ResponseStatus(HttpStatus.NO_CONTENT)
#PreAuthorize('isAuthenticated() && (principal.username == #username || principal.admin)')
void setValue(#PathVariable String some_param, #RequestBody String body_content) throws ValidationException, NotFoundException {
handleViolations(validate(AnObject, [some_param: some_param, body: body_content]))
try {
someService.setValue(some_param, body_content)
} catch(AlreadyExistsException e) {
throw new ValidationException([body: 'IN_USE'])
}
}
}
SomeControllerSpec.groovy < The test...
class AccountControllerSpec extends AbstractControllerSpec {
static final BASE_URL = 'http://localhost:8080/api/'
def client = new CustomRESTClient(BASE_URL)
// This test fails
def 'testing api'() {
//Expected 400 bad request but receiving a 406 not acceptable
client.put(
path: "/api/abc/fake_param/some_action",
// The body doesn't conform to the expectations of the API
body: 'blah',
contentType: MediaType.TEXT_PLAIN_VALUE
).status == HttpStatus.SC_BAD_REQUEST
// Exception thrown:
// INFO 22125 --- [tp1838490665-22] c.c.w.c.RestEndpointsConfiguration : org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
}
}
The Exception in the logs:
INFO 22125 --- [tp1838490665-22] c.c.w.c.RestEndpointsConfiguration : org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
I have tried many things including setting the expected header type:
client.setHeaders(accept: MediaType.TEXT_PLAIN_VALUE)
I have been trying various other things but to no avail. The exception persists.
Note: The action at the endpoint completes as expected.

Empty Exception Body in Spring MVC Test

I am having trouble while trying to make MockMvc to include the exception message in the response body. I have a controller as follows:
#RequestMapping("/user/new")
public AbstractResponse create(#Valid NewUserParameters params, BindingResult bindingResult) {
if (bindingResult.hasErrors()) throw BadRequestException.of(bindingResult);
// ...
}
where BadRequestException looks sth like this:
#ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "bad request")
public class BadRequestException extends IllegalArgumentException {
public BadRequestException(String cause) { super(cause); }
public static BadRequestException of(BindingResult bindingResult) { /* ... */ }
}
And I run the following test against /user/new controller:
#Test
public void testUserNew() throws Exception {
getMockMvc().perform(post("/user/new")
.param("username", username)
.param("password", password))
.andDo(print())
.andExpect(status().isOk());
}
which prints the following output:
Resolved Exception:
Type = controller.exception.BadRequestException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
MockHttpServletResponse:
Status = 400
Error message = bad request
Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Does anybody have an idea on why is Body missing in the print() output?
Edit: I am not using any custom exception handlers and the code works as expected when I run the server. That is, running the application and making the same request to the server returns back
{"timestamp":1423076185822,
"status":400,
"error":"Bad Request",
"exception":"controller.exception.BadRequestException",
"message":"binding failed for field(s): password, username, username",
"path":"/user/new"}
as expected. Hence, there is a problem with the MockMvc I suppose. It somehow misses to capture the message field of the exception, whereas the default exception handler of the regular application server works as expected.
After opening a ticket for the issue, I was told that the error message in the body is taken care of by Spring Boot which configures error mappings at the Servlet container level and since Spring MVC Test runs with a mock Servlet request/response, there is no such error mapping. Further, they recommended me to create at least one #WebIntegrationTest and stick to Spring MVC Test for my controller logic.
Eventually, I decided to go with my own custom exception handler and stick to MockMvc for the rest as before.
#ControllerAdvice
public class CustomExceptionHandler {
#ExceptionHandler(Throwable.class)
public #ResponseBody
ExceptionResponse handle(HttpServletResponse response, Throwable throwable) {
HttpStatus status = Optional
.ofNullable(AnnotationUtils.getAnnotation(throwable.getClass(), ResponseStatus.class))
.map(ResponseStatus::value)
.orElse(HttpStatus.INTERNAL_SERVER_ERROR);
response.setStatus(status.value());
return new ExceptionResponse(throwable.getMessage());
}
}
#Data
public class ExceptionResponse extends AbstractResponse {
private final long timestamp = System.currentTimeMillis();
private final String message;
#JsonCreator
public ExceptionResponse(String message) {
checkNotNull(message, "message == NULL");
this.message = message;
}
}
This likely means that you either didn't handle the exception or you've really left the body empty. To handle the exception either add an error handler in the controller
#ExceptionHandler
public #ResponseBody String handle(BadRequestException e) {
return "I'm the body";
}
or user the global error handler if you're on 3.2 or above
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler
public #ResponseBody String handleBadRequestException(BadRequestException ex) {
return "I'm the body";
}
}
with this the body will be populate, you should populate it with your error message
Updated solution:
If you don't want to do a full integration test but still want to make sure the message is as expected, you can still do the following:
String errorMessage = getMockMvc()
.perform(post("/user/new"))
...
.andReturn().getResolvedException().getMessage();
assertThat(errorMessage, is("This is the error message!");

Spring Boot Rest Controller how to return different HTTP status codes?

I am using Spring Boot for a simple REST API and would like to return a correct HTTP statuscode if something fails.
#RequestMapping(value="/rawdata/", method = RequestMethod.PUT)
#ResponseBody
#ResponseStatus( HttpStatus.OK )
public RestModel create(#RequestBody String data) {
// code ommitted..
// how do i return a correct status code if something fails?
}
Being new to Spring and Spring Boot, the basic question is how do i return different status codes when something is ok or fails?
There are several options you can use. Quite good way is to use exceptions and class for handling called #ControllerAdvice:
#ControllerAdvice
class GlobalControllerExceptionHandler {
#ResponseStatus(HttpStatus.CONFLICT) // 409
#ExceptionHandler(DataIntegrityViolationException.class)
public void handleConflict() {
// Nothing to do
}
}
Also you can pass HttpServletResponse to controller method and just set response code:
public RestModel create(#RequestBody String data, HttpServletResponse response) {
// response committed...
response.setStatus(HttpServletResponse.SC_ACCEPTED);
}
Please refer to the this great blog post for details: Exception Handling in Spring MVC
NOTE
In Spring MVC using #ResponseBody annotation is redundant - it's already included in #RestController annotation.
One of the way to do this is you can use ResponseEntity as a return object.
#RequestMapping(value="/rawdata/", method = RequestMethod.PUT)
public ResponseEntity<?> create(#RequestBody String data) {
if(everything_fine) {
return new ResponseEntity<>(RestModel, HttpStatus.OK);
} else {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
A nice way is to use Spring's ResponseStatusException
Rather than returning a ResponseEntityor similar you simply throw the ResponseStatusException from the controller with an HttpStatus and cause, for example:
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cause description here");
This results in a response to the client containing the HTTP status:
{
"timestamp": "2020-07-09T04:43:04.695+0000",
"status": 400,
"error": "Bad Request",
"message": "Cause description here",
"path": "/test-api/v1/search"
}
Note: HttpStatus provides many different status codes for your convenience.
In case you want to return a custom defined status code, you can use the ResponseEntity as here:
#RequestMapping(value="/rawdata/", method = RequestMethod.PUT)
public ResponseEntity<?> create(#RequestBody String data) {
int customHttpStatusValue = 499;
Foo foo = bar();
return ResponseEntity.status(customHttpStatusValue).body(foo);
}
The CustomHttpStatusValue could be any integer within or outside of standard HTTP Status Codes.
Try this code:
#RequestMapping(value = "/validate", method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<ErrorBean> validateUser(#QueryParam("jsonInput") final String jsonInput) {
int numberHTTPDesired = 400;
ErrorBean responseBean = new ErrorBean();
responseBean.setError("ERROR");
responseBean.setMensaje("Error in validation!");
return new ResponseEntity<ErrorBean>(responseBean, HttpStatus.valueOf(numberHTTPDesired));
}
There are different ways to return status code,
1 : RestController class should extends BaseRest class, in BaseRest class we can handle exception and return expected error codes.
for example :
#RestController
#RequestMapping
class RestController extends BaseRest{
}
#ControllerAdvice
public class BaseRest {
#ExceptionHandler({Exception.class,...})
#ResponseStatus(value=HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorModel genericError(HttpServletRequest request,
HttpServletResponse response, Exception exception) {
ErrorModel error = new ErrorModel();
resource.addError("error code", exception.getLocalizedMessage());
return error;
}
I think the easiest way is to make return type of your method as
ResponseEntity<WHATEVER YOU WANT TO RETURN>
and for sending any status code, just add return statement as
return ResponseEntity.status(HTTP STATUS).build();
For example, if you want to return a list of books,
public ResponseEntity<List<books>> getBooks(){
List<books> list = this.bookService.getAllBooks();
if(list.size() <= 0)
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
else
return ResponseEntity.of(Optional.of(list));
}

Resources