spring/hibernate validation -> error message is not passed to caller? - spring-boot

I am using org.springframework.boot:spring-boot-starter-validation:2.7.0(which in turn uses hibernate validator) to validate user input to rest controller.
I am using Spring Boot Web Starter (2.7.0) based project with #RestController annotation
My #GetMapping method is something like below -
#GetMapping(path = "/abcservice")
public Object abcService(
#RequestParam(value = "accountId", required = true) String accountId,
#Valid #RequestParam(value = "offset", required = false, defaultValue = "0") int offset,
#Valid #RequestParam(value = "limit", required = false, defaultValue = "10000") int limit
) throws Exception {
My problem is - I want the user to know about any input validation errors so they can correct and retry. But the framework is just giving 400 status code with below message.
{
"timestamp": "2022-08-03T16:10:14.554+00:00",
"status": 400,
"error": "Bad Request",
"path": "/somepath/abcservice"
}
On the server side the request is logged in warn.
2022-08-03 21:40:14.535 WARN 98866 --- [nio-8080-exec-1]
.w.s.m.s.DefaultHandlerExceptionResolver : Resolved
[org.springframework.web.method.annotation.MethodArgumentTypeMismatchException:
Failed to convert value of type 'java.lang.String' to required type
'int'; nested exception is java.lang.NumberFormatException: For input
string: "0s"]
I want this above error message --> Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: "0s" also to be passed on to user. Is there a easy configuration based way to achieve.
I think i can add a ControllerAdvice to handle this exception and include this message in the response from handler method. But this will be a couple of lines of code. Is there an even simpler way than the ControllerAdvice approach.
Similarly if the client don't pass the mandatory accountId param, the client is just getting the same 400 response as above. No details or hints to the client about what wrong they are doing or how they can fix it.. but on the server side i can see below warn log.
2022-08-03 21:59:20.195 WARN 235 --- [nio-8080-exec-3]
.w.s.m.s.DefaultHandlerExceptionResolver : Resolved
[org.springframework.web.bind.MissingServletRequestParameterException:
Required request parameter 'accountId' for method parameter type
String is not present]
I want the client to know about this error/exception. Nothing secret here to hide (atleast in my case).
Edit - found this config -
server.error.include-message=always
Now the issue is, bad requests are sent with 500 status code, I want them to sent as 400. Then this Q is solved.
Validations made by #Valid return with 500 Status Code. Is there anyway to tell the server to return 400 response when validations fail (without using ControllerAdvice).
If you wish to test-- you can try -->
Annotate controller with #Validated.
And execute below method and you will see 500 error but would want this to be 400.
#GetMapping("/test")
public void test(#Valid #RequestParam(value = "studentId", required = false)
#Min(value=0, message="Can not be less than 0") #Max(value=200, message="Can not be above 200") Long studentId ) {
System.out.println("hit: ");
}
And hit - http://localhost:9099/test?studentId=400

The spring in-built solution without global exception handler and with minimal config is by adding the below property in the application.properties.
server.error.include-binding-errors=always
The above property can have three values:
always ----> All api's in the app will always return well defined validation error message response.
on-param ----> All api's in the app will conditionally return well defined validation error message response based on input request param field "errors"
never ---> to disable this feature.
Example Github Project Reference
Demo Test:
package com.example.demo;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
#SpringBootApplication
#RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#PostMapping("/test")
public void test(#Valid #RequestBody Student student) {
System.out.println("studentData: " + student);
}
}
class Student {
#NotNull(message = "firstName cannot be null")
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
#Override
public String toString() {
return "Student [firstName=" + firstName + ", lastName=" + lastName + "]";
}
}
Request:
{
"firstName": null,
"lastName" : "sai"
}
Response: (with HTTP response code = 400)
{
"timestamp": "2022-08-04T05:23:58.837+00:00",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotNull.student.firstName",
"NotNull.firstName",
"NotNull.java.lang.String",
"NotNull"
],
"arguments": [
{
"codes": [
"student.firstName",
"firstName"
],
"arguments": null,
"defaultMessage": "firstName",
"code": "firstName"
}
],
"defaultMessage": "firstName cannot be null",
"objectName": "student",
"field": "firstName",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
],
"path": "/test"
}

Use #expection handler and controller advice this help to handle your issue

Related

Why do I get random Http 404 from server between same requests with only one change in any field?

I haven an endpoint POST /api/marketplace/add that accepts a DTO object as request body. When I send the body below with platformName field set , server accepts request and processes it with no problem. But when I only try to change field platformName to null I get Http 404 error from server. I debugged the request and found out that it even can not reach controller method. I also got no trace from that error. What might be the cause that makes API respond differently to same request?
below
{
"platformName": "Trendyol",
"commissionAmounts": [
{
"amount": 23.45,
"categoryInfos": [
{
"categoryName": "Game"
}
],
"isCategoryBasedPricing": true
}
],
"shipmentAmounts": [
{
"amount": 23.45,
"scaleInfo": {
"order": 0,
"lowerBound": 0,
"upperBound": 0
},
"volumeInfo": {
"order": 0,
"lowerBound": 0,
"upperBound": 0
},
"isVolumeBasedPricing": true
}]
}
EDIT: dto model is
#Generated
public class MarketPlaceDTO {
#JsonProperty("platformName")
private String platformName;
#JsonProperty("commissionAmounts")
#Valid
private List<CommissionInfoDTO> commissionAmounts = new ArrayList<>();
#JsonProperty("shipmentAmounts")
#Valid
private List<ShipmentInfoDTO> shipmentAmounts = new ArrayList<>();
Controller is implementing swagger generated api interface. with postmapping and requestbody annotations.
#RequiredArgsConstructor
#RestController
public class MarketPlaceApiController implements MarketplaceApi {
private final MarketPlaceDAOService marketPlaceDAOService;
#Override
public ResponseEntity<BaseResponseDTO> addMarketPlace(MarketPlaceDTO
marketPlaceDTO) {
BaseResponseDTO dto =
marketPlaceDAOService.addMarketPlace(marketPlaceDTO);
return ResponseEntity.ok(dto);
}
}
Swagger generated api interface
#RequestMapping(
method = RequestMethod.POST,
value = "/marketplace/add",
produces = { "application/json", "application/xml" },
consumes = { "application/json" })
default ResponseEntity<BaseResponseDTO> _addMarketPlace(
#Parameter(name = "MarketPlaceDTO", description = "Add new
marketplace with given request body", required = true) #Valid
#RequestBody MarketPlaceDTO marketPlaceDTO) {
return addMarketPlace(marketPlaceDTO);
}
Response is
{
"timestamp": 1666866382906,
"status": 404,
"error": "Not Found",
"path": "/marketplace/add"
}
Obviously, that you use an endpoint with #RequestBody where body is a DTO.
And on trying to call this endpoint Spring Web first should match that a model in your request payload matches a require object in #RequestBody argument.
Ideally, using DTO as a request model is not a good idea. But I don't see your structure and cannot say if it's a problem or not.
The simple solution in your case is preparation (annotating) your DTO with specific JSON annotations:
#JsonInclude
#JsonIgnoreProperties(ignoreUnknown = true)
public class YourDTO {
private String platformName;
}
and for Controller add class annotation #Validated; for #RequestBody add #Valid annotation.
Recommendation: use request models for incoming objects, and later converters to DTO/entities with ability to response them with filtering (or in complex cases add also response model - usually it's overhead).
My problem was global exception handler component annotated with #ControllerAdvice. I tried to handle validation exceptions and forgot to add #ResponseBody to my handler methods which is in my case probabaly required. That somehow caused server to send http 404 message when any input validation exception was thrown. After I made changes , Exceptions was handled correctly by handler component.
#ControllerAdvice
#ResponseBody // this resolved my issue.
public class MVCExceptionHandler {
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseErrorResponse
methodArgumentExceptions(MethodArgumentNotValidException e){
return BaseErrorResponse.builder()
.errorMessage(AppError.INVALID_OR_MISSING_USER_INPUT.getErrorMessage())
.errorCode(AppError.INVALID_OR_MISSING_USER_INPUT.getErrorCode())
.errorTime(Date.from(Instant.now())).build();
}

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());
}

I'm trying to authorize in PostMan, but it gives me "Request method 'GET' not supported"

I'm trying firstly just output the dates from POST method in '/login' cause I'm not sure the correctness of my code. I hope you will help me, thanks.
MainController.java
#RestController
public class MainController {
#RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(#RequestBody Credentials credentials) {
return "username: " + credentials.getUsername() + " password: " + credentials.getPassword();
}
}
PostMan query
[![Postman dropdown list][1]][1]
[Screenshot link, if there is no picture above][1]
{
"username": "admin",
"password": "admin"
}
I've tried send dates as a row (JSON) and as form, but in anyway it gives me these error
{
"timestamp": "2020-03-29T10:03:20.711+0000",
"status": 405,
"error": "Method Not Allowed",
"message": "Request method 'GET' not supported",
"path": "/login"
}
I notice, that in compiler throws me this error "org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
", after that gave me this "Request method 'GET' not supported"
As mentioned in answers, selecting the POST method from the dropdown in Postman will help with the following error:
"Request method 'GET' not supported."
Then you will face the error below:
{
"timestamp": "2020-03-28T16:54:55.288+0000",
"status": 400,
"error": "Bad Request",
"message": "Required request body is missing: public java.lang.String com.example.demo.controller.MainController.login(java.lang.String,java.lang.String)",
"path": "/login"
}
To solve this, you should slightly modify the endpoint:
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class MainController {
#RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(#RequestBody Credentials credentials) {
return "username: " + credentials.getUsername() + " password: " + credentials.getPassword();
}
}
public class Credentials {
private String username;
private String password;
private Credentials() {
}
// getters and setters omitted, make sure you have them.
}
#RequestBody annotation expects a JSON object to deserialize. It is necessary to have an object available for mapping.
Because you are using #RestController annotation, there is no need for #ResponseBody above the method. It is already included.
You should pick "POST" method from the METHOD dropdown:
Can you provide a screenshot of postman ? because i think you didn't choose "POST" from the dropdown list next to the URL. it should be GET by default
Try request body x-www-form-urlencoded
In some reasons in front of MainController there is must be "#RequestMapping()", it works for me
try #RequestBody(required=false)
or mentioned request body x-www-form-urlencoded

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")));
...
}

How to handle the bind exception with #ExceptionHandler

My requirement is to perform the server side validation for the form using Spring 3.0 and Hibernate Validator.Remember that I am submitting the form using AJAX call.My Controller class code is like below.
public ModelAndView generatePdfReport(#ModelAttribute("reports") #Valid ReportsCommand model, BindingResult result, ModelAndView modelAndView,
HttpServletRequest request, HttpServletResponse response) throws Exception {
if (result.hasErrors()) {
throw new BindException(result);
}
else{
...
}
update...
#ExceptionHandler(BindException.class)
public #ResponseBody String handleException(BindException e,HttpServletRequest request, HttpServletResponse response)
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return e.getMessage();
}
This is the Handler Method I placed in the controller.I used the #ResponseBody annotation but still it is showing the response in html format not in JSON format...
What is the wrong in my code..
And the below is the field I am validating
#Size(min = 2, max = 3, message = "calltype must between 2 to 3 Characters.")
private String callType;
If I give size as more than three, it is entering into the if and throwing the exception.What I want is that, I want to handle this exception and return the json response.May be I can do this using #ExceptionHandler but don't know how.Or any other solution to resolve this problem also will be greatly appreciated.
There is no automatic way to transform the binding errors to JSON. You should do that manually. You can do it in two places:
inline - instead of throwing BindException, generate the JSON and return it (using a custom ModelAndView that works with JSON, or by writing to the response)
in an exception handler declared to handle BindException. You annotate a method of some (base) controller with #EXceptionHandler(BindException.class) and do the same transformation errors -> json as above
import this package
import org.springframework.validation.BindException
not this
import java.net.BindException
In my spring boot version(2.2.4.RELEASE), there is a method you can override under the class(MyCustomExceptionHandler) extended by ResponseEntityExceptionHandler.
The method that you can use like that:
#Override
protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
logger.info(ex.getMessage());
return super.handleBindException(ex, headers, status, request);
}
You can return ResponseEntity that includes related object. I just printed the exception message in the log as an example.
Just to give an update about the current version of Spring:
If you just simply throw a BindException(bindingResult) from your conroller method, then spring will return a detailed JSON reply with information about all the validation errors:
Method in #RestController
#RequestMapping(value = "/ballot", method = POST)
public BallotModel postBallot(#Valid #RequestBody BallotModel newBallot, BindingResult bindingResult) throws BindException {
log.trace("=> POST /ballot "+newBallot);
log.debug("ballotService="+ballotService);
if (bindingResult.hasErrors()) {
log.trace(" ballot is invalid: "+bindingResult.getAllErrors());
throw new BindException(bindingResult); // this generates a cool error message. But it should be documented :-)
}
return newBallot;
}
HTTP reply
{
"timestamp": "Sep 20, 2016 11:57:07 AM",
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.validation.BindException",
"errors": [
{
"field": "voteOrder",
"bindingFailure": false,
"objectName": "ballotModel",
"codes": [
"NotNull.ballotModel.voteOrder",
"NotNull.voteOrder",
"NotNull"
],
"arguments": [
{
"codes": [
"ballotModel.voteOrder",
"voteOrder"
],
"defaultMessage": "voteOrder"
}
],
"defaultMessage": "may not be null"
}
],
"message": "Validation failed for object='ballotModel'. Error count: 1",
"path": "/ballot"
}

Resources