#ControllerAdvice handle the exception but not customize response - spring

This is my exception handler
#ControllerAdvice(basePackageClasses = [SignUpController::class])
class ControllerAdvice: ResponseEntityExceptionHandler() {
#ExceptionHandler(Exception::class)
#ResponseBody
fun handleControllerException(request: HttpServletRequest, ex: Throwable): ResponseEntity<*> {
val status = HttpStatus.CONFLICT
return ResponseEntity<Any>(ApiError(status.value(), ex.message), status)
}
}
and my custom class
data class ApiError(val status: Int, val message: String?)
The handler work but error is throw like follow
{
"timestamp": "2019-01-29T19:17:22.541+0000",
"status": 401,
"error": "Unauthorized",
"message": "could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement",
"path": "/sign-up"
}
But I'm expected for something like follow
{
"apierror": {
"status": ...,
"message": ..."
}
}
I'm based for this tutorials
https://www.toptal.com/java/spring-boot-rest-api-error-handling
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-error-handling
https://medium.com/#jovannypcg/understanding-springs-controlleradvice-cd96a364033f
What I'm doing wrong? I'm missing any config?

You need to annotate your ApiError class with the following:
#Data
#JsonTypeInfo(
include = JsonTypeInfo.As.WRAPPER_OBJECT,
use = JsonTypeInfo.Id.CUSTOM,
property = "error",
visible = true
)
#JsonTypeIdResolver(LowerCaseClassNameResolver.class)

My workaround was, but is not the correct answer
on application.yaml
server:
error:
include-exception: true
on my controller I'm check if email and username exists and throw a custome message to exception
#RestController
#RequestMapping("/register")
class RegisterController: BaseController() {
#ResponseStatus(HttpStatus.CREATED)
#PostMapping("/save")
fun register(#RequestBody user: ApplicationUser) {
if (userRepository.existsByEmail(user.email)) {
throw DataIntegrityViolationException("Email already exists")
}
if (userRepository.existsByUsername(user.username)) {
throw DataIntegrityViolationException("Username already exists")
}
user.password = bCryptPasswordEncoder.encode(user.password)
userRepository.save(user)
}
}
and the return worked fine
{
"timestamp": "2019-01-31T17:08:40.832+0000",
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.dao.DataIntegrityViolationException",
"message": "Email already exists", //here is
"path": "/register/save"
}

Related

Change default "type" for problem detail API in Spring Boot?

I'm using Spring Boot 3.x and I have a controller defined like this:
#RestController
#RequestMapping(path = ["/my-controller"])
#Validated
class MyController {
private val log = loggerFor<MyController>()
#PutMapping("/{something}", consumes = [APPLICATION_JSON_VALUE])
#ResponseStatus(code = HttpStatus.NO_CONTENT)
fun test(
#PathVariable("something") something: String,
#Valid #RequestBody someDto: SomeDTO
) {
log.info("Received $someDto")
}
}
data class SomeDTO(val myBoolean: Boolean)
I've also enabled problem details (RFC 7807) in my application.yaml file:
spring:
mvc:
problemdetails:
enabled: true
When I make a request (in this example I'm using rest assured) to /my-controller/hello with a json body that (intentionally) doesn't match the expected data (myBoolean is not a valid boolean):
Given {
port(<port>)
contentType(JSON)
body("""{ "myBoolean" : "not a boolean"}""")
log().all()
} When {
put("/my-controller/hello")
} Then {
log().all().
statusCode(400)
}
Then the response body looks like this:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Failed to read request",
"instance": "/my-controller/hello"
}
My question is, how can I change the default type from about:blank to something else?
You need a #ControllerAdvice defined as follows:
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(Exception.class)
public ResponseEntity<Problem> handleAllExceptions(Exception ex, WebRequest request) {
return ResponseEntity.of(
Optional.of(
Problem.builder()
.withType(URI.create("https://foobar.com/problem-definitions/blah"))
.withTitle("Bad Request")
.withDetail(ex.getMessage())
.withStatus(Status.BAD_REQUEST)
.build()
));
}
}
It returns this for your example:
{
"type": "https://foobar.com/problem-definitions/blah",
"title": "Bad Request",
"status": 400,
"detail": "Type definition error: [simple type, class com.example.demo.web.LanguageController$SomeDTO]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.demo.web.LanguageController$SomeDTO`: non-static inner classes like this can only by instantiated using default, no-argument constructor\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 3]"
}
I used this dependency:
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web-starter</artifactId>
<version>0.28.0-RC.0</version>
</dependency>
Note that ProblemDetail is Spring Framework 6. Implementation in Spring 6 looks like this:
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(Exception.class)
public ResponseEntity<ProblemDetail> handleAllExceptions(Exception ex, WebRequest request) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
problemDetail.setType(URI.create("https://foobar.com/problem-definitions/blah"));
problemDetail.setInstance(URI.create("https://instance"));
return ResponseEntity.of(Optional.of(problemDetail));
}

Webclient onStatus does not work in case of 406 returned from downstream API

I'm doing a onStatus implementation in my API when I use a webclient (Webflux) to call external API:
//Webclient Call
Flux<Movie> movies = webclient.get().uri(uriBuilder -> uriBuilder.path(api_url)
.build(author))
.retrieve()
.onStatus(HttpStatus::is4xxClientError,
response -> Mono.error(new AcceptHeaderNotsupportedException(response.statusCode().getReasonPhrase())))
.bodyToFlux(Movie.class)
//Global Handler Exception Class
public class GlobalExceptionHandler {
#ExceptionHandler(AcceptHeaderNotsupportedException.class)
public ResponseEntity<?> AcceptHeaderHandling(AcceptHeaderNotsupportedException exception){
ApiException apiException = new ApiException(HttpStatus.NOT_FOUND.value(), exception.getMessage());
return new ResponseEntity<>(ApiException, HttpStatus.NOT_FOUND);
}
}
//AcceptHeaderNotsupportedException Class
public class AcceptHeaderNotsupportedException extends RuntimeException{
public AcceptHeaderNotsupportedException(String message){
super(message);
}
}
//Api custom Exception
public class ApiCustomException{
private int code;
private String message;
}
I am testing a scenario webclient call that return a 406 error from downstream api. So i want to map the response to my object representation and give to my client (postman in this case).
{
code: 406,
"message": error from downstream api
}
but i am getting to client
{
"timestamp": "2021-08-29T14:31:00.944+00:00",
"path": "path",
"status": 406,
"error": "Not Acceptable",
"message": "Could not find acceptable representation",
"requestId": "ba66698f-1",
"trace": "org.springframework.web.server.NotAcceptableStatusException: 406 NOT_ACCEPTABLE \"Could not find acceptable representation\"\n\tat ....}
In case of a 404 error from downstream API the mapping response works fine.
{
code: 404,
"message": not found
}
My question is if i am doing .onStatus(HttpStatus::is4xxClientError should not work for both (404, 406 or other responde code with 4xx ?

Exception message not included in response when throwing ResponseStatusException in Spring Boot

My Spring Boot application provides the following REST controller:
#RestController
#RequestMapping("/api/verify")
public class VerificationController {
final VerificationService verificationService;
Logger logger = LoggerFactory.getLogger(VerificationController.class);
public VerificationController(VerificationService verificationService) {
this.verificationService = verificationService;
}
#GetMapping
public void verify(
#RequestParam(value = "s1") String s1,
#RequestParam(value = "s2") String s2) {
try {
verificationService.validateFormat(s1, s2);
} catch (InvalidFormatException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
}
}
}
In case validateFormat() throws the InvalidFormatException the client gets a HTTP 400 which is correct. The default JSON response body however looks like this:
{
"timestamp": "2020-06-18T21:31:34.911+00:00",
"status": 400,
"error": "Bad Request",
"message": "",
"path": "/api/verify"
}
The message value is always empty even if I hard-code it like this:
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "some string");
This is the exception class:
public class InvalidFormatException extends RuntimeException {
public InvalidFormatException(String s1, String s2) {
super(String.format("Invalid format: [s1: %s, s2: %s]", s1, s2));
}
}
This behavior has changed with Spring Boot 2.3 and is intentional. See release notes for details.
Setting server.error.include-message=always in the application.properties resolves this issue.
Setting server.error.include-message=always disclosures messages of internal exceptions and this might be a problem in production environment.
An alternative approach is to use ExceptionHandler. Here you can control what is transferred to client:
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<String> handleBadRequestException(ResponseStatusException ex) {
// if you want you can do some extra processing with message and status of an exception
// or you can return it without any processing like this:
return new ResponseEntity<>(ex.getMessage(), ex.getStatus());
}
}

Spring Boot catch multiple exceptions and send as error response

I am validating an incoming POST request which will create a database entity after validating the request data. I am trying to gather multiple errors in a single request and respond as error response following JSON API spec:
https://jsonapi.org/examples/#error-objects-multiple-errors
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
"errors": [
{
"status": "403",
"source": { "pointer": "/data/attributes/secretPowers" },
"detail": "Editing secret powers is not authorized on Sundays."
},
{
"status": "422",
"source": { "pointer": "/data/attributes/volume" },
"detail": "Volume does not, in fact, go to 11."
},
{
"status": "500",
"source": { "pointer": "/data/attributes/reputation" },
"title": "The backend responded with an error",
"detail": "Reputation service not responding after three requests."
}
]
}
Is it possible to do this by #ControllerAdvice. When Global exception handling is enabled by #ControllerAdvice and throws an exception, the next exception won't be caught.
Not directly, no. Not sure what is your business case/logic, therefore I don't know how you handling these exceptions in service layer, but in general, if you want to pass multiple errors in your #ExceptionHanlder - you could create a custom POJO:
public class MyError {
private String status;
private String source;
private String title;
private String detail;
getters/setters...
}
and then create a custom RuntimeException which would accept list of these POJOs:
public class MyRuntimeException extends RuntimeException {
private final List<MyError> errors;
public MyRuntimeException(List<MyError> errors) {
super();
this.errors = errors;
}
public List<MyError> getErrors() {
return errors;
}
}
And in your service layer you could create list of these POJOs, wrap then in your exception and throw it. Then in #ControllerAdvice you simply catch your exception and call accessor method to iterate against your list of POJOs to construct a payload you want.
Something like:
#ExceptionHandler (MyRuntimeException.class)
#ResponseStatus (BAD_REQUEST)
#ResponseBody
public Map<String, Object> handleMyRuntimeException(MyRuntimeException e) {
return singletonMap("errors", e.getErrors());
}

How to display customized error response in REST API

My url is http://localhost:8090/employee/?emp_id=1551&name=
I am using Spring boot for designing REST application. I have used #RequestMapping and #RequestParam annotation for get resource. When I pass empty value to request parameter (for eg. name = ), I get below validation response(actual output section below).
However I wanted to override this output to display customized error response as below(expected section below).
How can I achieve this? How to avoid Spring's auto validation for input parameters in Get request?
Output
======
{
"timestamp": 1511144660708,
"status": 400,
"error": "Bad Request",
"message": "Required String parameter 'name' is not present",
"path": "/employee"
}
Expected
========
{
"errors":[
{
"id":"123144",
"detail": "invalid user input"
"status": "400"
}
]
}
Following sample code demonstrates how to customize error message for exception handling.
Create 2 POJOs for your customized response body.
Implement 1 method to catch the MissingServletRequestParameterException exception with #ExceptionHandler annotation for missing paramters.
Generate the response as you expected.
Class: ResponseProperty.java
public class ResponseProperty {
private int id;
private String detail;
private int status;
//getters and setters produced by IDE
}
Class: ResponsePOJO.java
public class ResponsePOJO {
List<ResponseProperty> errors;
public List<ResponseProperty> getErrors() {
return errors;
}
public void setErrors(List<ResponseProperty> errors) {
this.errors = errors;
}
}
Method: handleMethodArgumentTypeMismatch
#ExceptionHandler({ MissingServletRequestParameterException.class })
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MissingServletRequestParameterException ex) {
ResponseProperty property = new ResponseProperty();
property.setId(123144);
property.setDetail("invalid user input");
property.setStatus(400);
ResponsePOJO responsePOJO = new ResponsePOJO();
List<ResponseProperty> propertyList = new ArrayList<ResponseProperty>();
propertyList.add(property);
responsePOJO.setErrors(propertyList);
return new ResponseEntity<Object>(responsePOJO, HttpStatus.BAD_REQUEST);
}
If you visit the endpoint /employee without required parameter, then you are going to see the response as follows:
Http Response
{
"errors": [
{
"id": 123144,
"detail": "invalid user input",
"status": 400
}
]
}
Hope this helps you! :)
UPDATE
If you want to get the request ID from header named requestId for response, you can use WebRequest to get this information as follows:
#ExceptionHandler({ MissingServletRequestParameterException.class })
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MissingServletRequestParameterException ex,
WebRequest request) {
ResponseProperty property = new ResponseProperty();
property.setId(Integer.valueOf(request.getHeader("requestId")));
...
}

Resources