I offer a REST webservice using spring #RestController.
How should in general invalid parameter content be handled? I tried throwing a custom exception, but that will lead to a HTTP 500 error on the client side and expose the stacktrace thereby.
Probably that is not the right way. But how should just simple error messages be returned? (the webservice will not be accessed manually by users. Just by other services connecting to the rest controller).
Im using jersey and this is a simple example that will use the hibernate bean validation framework to validate your beans. This is a work in progress but you should can see how it will work very simply.
#Path("customers")
public class CustomerResource {
#PUT
public Response createCustomer(Customer customer) {
BeanValidator.validate(customer);
final String rialtoId = customerProvider.createCustomer(customer);
return Response.ok(rialtoId).build();
}
}
Here is a generic class that I created that handles the bean validation.
public class BeanValidator {
/**
* Used to validate an order request and all the attached objects that
* support validation.
*
* #param request
* #throws ConstraintViolationException
*/
public static <T> void validate(T request) throws ConstraintViolationException {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<T>> constraintViolations = validator.validate(request);
if (constraintViolations.size() > 0) {
throw new ConstraintViolationException(new HashSet<ConstraintViolation<?>>(constraintViolations));
}
}
}
#XmlRootElement
public class Customer {
#NotNull(message = "spCustomerID1 is a required field")
#Size(max = 60, message = "spCustomerID1 has a max length of 60 characters")
private String spCustomerID1;
#Size(max = 60, message = "spCustomerID2 has a max length of 60 characters")
private String spCustomerID2;
#Size(max = 60, message = "spCustomerID3 has a max length of 60 characters")
private String spCustomerID3;
#NotNull(message = "customerName is a required field")
#Size(max = 60)
private String customerName;
#Valid
#NotNull(message = "customerAddress is a required field")
private PostalAddress customerAddress;
#Valid
#NotNull(message = "customerContact is a required field")
private ContactInfo customerContact;
#Valid
#NotNull(message = "technicalContact is a required field")
private ContactInfo technicalContact;
... / Getters and Setters
}
Then here is a simple ExceptionMapper that will support constructing a simple response to be sent back to the client. Notice that it will set the Response type to a 400 BAD_REQUEST instead of a 500+ Server Side error.
public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
public Response toResponse(ConstraintViolationException exception) {
final StringBuilder strBuilder = new StringBuilder();
for (ConstraintViolation<?> cv : exception.getConstraintViolations()) {
strBuilder.append(cv.getPropertyPath().toString() + " " + cv.getMessage());
}
RestResponse responseEntity = RestResponse.responseCode(ResponseCode.CONSTRAINT_VIOLATION).setResponseMessage(strBuilder.toString()).build();
return Response.status(Response.Status.BAD_REQUEST).entity(responseEntity).build();
}
}
This code hasn't been tested yet but it might help to get some ideas of how to do the validation. This is a pretty straight forward way to do rest service validation in my opinion and allows you to report exact variable paths along with customized error messages for each field.
You should validate your parameters at the very outmost layer of your application before it gets handed off inside your domain. At this point you're still in the HTTP layer so can take the appropriate action which is to return a 400 BAD REQUEST status.
Within that you have complete control over how to relay this information to your users (or other services). Plain text is fine if you're just logging it, or design your own Json/Xml payload describing the error.
if i understand you well,
then , generally i think it's good to have a key in each json response (or even if your response is XML), that indecates the status of the process. this field could be called status.
so each and every response you send back should have this status field, and it's value should indicate what happens while processing, and what should the caller expect in the response.
the value could be a number, or a text message, some constant-like message
also you can add another field, message that contains some text-desc of the status code.
now you have to make a list of possible statues that your service may send back.
ex:
status: 0000
message: success
status: 0001
message: invalid_params
status: 0002
message: invalid_param_value
status: 0003
message: missing_param,
:
:
etc
so your json response will contain those fields always. among the other data supposed to be returned.
now it's the clients duty to handle those responses.
JSON example:
{
"status":"0000",
"message":"success",
"images":[ ... ]
}
{
"status":"0003",
"message":"missing_param"
}
as you notice in case of non 0000 status, no other data is sent back.
just telling the client we have "this issue".
or you can make it more informative, by adding : to the error message constant, telling more info about the error:
ex,
{
"status":"0003",
"message":"missing_param:album_id"
}
telling the user, there is a missing parameter, and it's album_id
now you can write all possible status responses, and there message
this will be part of your service documentation.
I you manually validate your arguments, you could throw a specific exception. Then you can map your exception to a specific HTTP status, for instance BAD REQUEST.
You can map your exception to a response status with Spring Controller Advice: http://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc.
Related
I have an API endpoint that get a name and description parameters (both are mandatory)
createSomething(#RequestParam(value = "name") String name,#RequestParam(value = "description") String description)
If the client is not providing any of these he will get 400 Bad Request
Is there a way for me to tell the client which field is missing ? give more information for the "Bad Request" response
Update: Note that the parameters must be mandatory since I want that OpenAPI will detect that these parameters are mandatory. So solutions like making then "optional" and checking inside the body of the function is not what I am looking for
I see multiple answers but no one is specific enough.
1)
Spring by default has the capability of reporting in the error message what parameter was missing or other violations in the request.
However since spring boot version 2.3 the specific error messages are hidden, so that no sensitive information can be disclosed to the user.
You can use the property server.error.include-message: always which was the default mechanism before 2.3 version and allow spring to write error messages for you again.
2)
If you can't afford this because other sensitive info could be leaked from other exceptions, then you have to provide your own exception handler for this specific case
The following can be placed either in the same controller, or in another class marked with #ControllerAdvice
#ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity handleMissingParams(MissingServletRequestParameterException ex) {
return ResponseEntity.badRequest().body(String.format("Missing parameter with name:%s", ex.getParameterName()));
}
As #Shubam said, you can use the defaultValue attribute of #RequestParam annotation by setting the required attribute to true since both the parameters are mandatory.
Here is an example of how you could do it,
private final String DEFAULT_NAME = "Default Name";
private final String DEFAULT_DESC = "Default Desc";
#RequestMapping(value = "/get", method = RequestMethod.GET)
#ResponseStatus(HttpStatus.OK)
public ResponseEntity<String> createSomething(#RequestParam(required = true, name = "name", defaultValue = "Default Name") String name,
#RequestParam(required = true, name = "description", defaultValue = "Default Desc") String desc){
if(DEFAULT_NAME.equals(name)){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Field Name is missing");
}
if(DEFAULT_DESC.equals(desc)){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Field Desc is missing");
}
return ResponseEntity.ok(String.format("Hello, %s!",name));
}
You can use validation with a customised message :
#GetMapping("/name-for-month")
public String getNameOfMonthByNumber(#RequestParam #Min(1) #Max(value = 12, message = “month number has to be less than or equal to 12”) Integer month) {
// ...
}
There are many ways of handling errors for Rest find below a link of at least 5 solutions for your issue :
ExceptionHandler
HandlerExceptionResolver (ResponseStatusExceptionResolver this is the most adducate for your case or the 4th one if you use spring 5+)
ControllerAdvice
ResponseStatusException
Handle the Access Denied in Spring Security
https://www.baeldung.com/exception-handling-for-rest-with-spring
Since both parameters are mandatory you'll be getting 400 (bad request) if you try to send the request without paramters.
A workaround could be making request parameters non-mandatory(so that request can be sent without parameters) and provide a default value in case no parameter is provided
createSomething(#RequestParam(value = "name", required=false, defaultValue = null) String name,#RequestParam(value = "description", required=false, defaultValue = null) String description)
In the function, you can check for null like the following -
if (name == null) // name parameter is not provided
if (description == null) // description paramter is not provided
And, based on conditions you can also send error reponse if any one/more paramter not provided in the request.
I'm trying to present an actual message with a failed validation of an #Future Bean Validation annotation but have been unsuccessful thus far. Here is my current code:
public class ForecastDateVO {
#Future(message = "The forecast date must be in the future.") // Message desired for display
private Date forecastDate;
public Date getForecastDate() {
return this.forecastDate;
}
public void setForecastDate(final Date forecastDate) {
this.forecastDate = forecastDate;
}
}
where it is put to use like this:
#PUT
#Path("whatever/{projectId}")
public void updateForecastDate(#PathParam("projectId") #NotNull final long projectId,
#Valid final ForecastDateVO vo) {
// Something absolutely amazing
}
Now the validation is actually working with the #Valid tag applied; when I make the rest call with a date in the payload that is not in the future I receive a "400 Bad Request". However, I would like to actually see the error message display in the annotation above.
Is this possible...and if so what's the best way to accomplish it?
Thanks for your time!
I'm using a bit of a personalized security back-end due to the nature of the app and was trying out how to implement a few simple error returns in my REST API controller. It's simple enough to do in a html page controller like I have in the following:
#Controller
public class HomeController {
#Autowired
private UserService userService;
#GetMapping("/home.html")
public String home(Model model) {
String redirect = "home";
if(!userService.getCurrentUser().isCanAccessService()) {
redirect = "unauthorized";
}
return redirect;
}
}
I can easily just redirect it to the unauthorized page that I made since I'm returning the string value here. However, when I go to a REST API it's not as simple:
#RestController
public class bagelController {
#Autowired
private bagelService bagelService;
#Autowired
private UserService userService;
#GetMapping("/rest/bagel/search")
public Bagel searchBagel (#RequestParam(value = "bagel", required = false) String bagel,
#RequestParam(value = "bagelInd", required = false, defaultValue = "1") int bagelInd) {
Bagel bagel;
if(!userService.getCurrentUser().isBagelEditAccess()) {
bagel = null;
// I want to return a 401 or direct to my unathorized page if I get an invalid user here.
}
else {
bagel = bagelService.getbagel(bagel, bagelInd);
// if my bagel object returns null, I want to return a 404 or direct to a 404 not
found page here.
}
return bagel;
}
You can have a ControllerAdvice which handles exceptions and their HTTP return code. Then you can annotate a method in it the following way for example:
#ExceptionHandler(NoSuchEntityException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
This will return a 404 code every time it encounters a NoSuchEntityException (custom exception). So you can throw such an exception when you check if an entity is null. You can use the same thing for 401 or any other HTTP code as well.
One way to do this.
#GetMapping("/rest/bagel/search")
public ResponseEntity<Bagel> searchBagel (#RequestParam(value = "bagel", required = false) String bagel,
#RequestParam(value = "bagelInd", required = false, defaultValue = "1") int bagelInd) {
Bagel bagel = null;
if(!userService.getCurrentUser().isBagelEditAccess()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
else {
bagel = bagelService.getbagel(bagel, bagelInd);
if(bagel == null) {
return ResponseEntity.notFound().build();
}
}
return ResponseEntity.ok(bagel);
}
You can create custom exceptions within your application for this scenario like BagelNotFoundException and UnauthorizedException. Both these custom exception classes can extend Exception class or more specific classes from java exception hierarchy. You can annotate these custom exception classes with #ResponseStatus annotation to provide the http status code that should be sent in the response.
Next, you need to throw the objects of these exceptions within your controller.
Once this exception is thrown, an exception handler should be present within your application to take care of these exceptions. The same can be defined using #ControllerAdvice and #ExceptionHandler within your custom exception handler classes.
This way you'll be able to send appropriate response to the client, and the client application needs to redirect the user to error pages based on the response code received.
Hope this helps!
I have a Java Pojo class which is expected as a #RequestBody in my Rest Controller. This Java Pojo class has an Integer field.
When user is calling the Rest API and they pass Integer field value as a Junk String, then Jackson is throwing InvalidFormatException. I instead want to use Javax Validator framework annotation to handle this error and show a meaningful error message in response. Unfortunately Jackson deserialization from JSON to Java happens before Javax validation, therefore my #Digits validation never gets invoked.
Note, #NotNull gets invoked but #Digits is not getting invoked because jackson fails the request much before the call reaches Javax validation layer IMO.
Java Pojo class:
public class Board implements Serializable{
private static final long serialVersionUID = 1L;
#Digits(message = "quantity must be a number", integer = 8, fraction = 0)
private Integer quantity;
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity= quantity;
}
}
Controller class:
#Controller
#RequestMapping("boards")
public class EnrichController {
#Autowired
private BoardService boardService;
#RequestMapping(value = "", method = RequestMethod.PUT, produces = "application/json;charset=UTF-8")
public #ResponseStatus(value = HttpStatus.NO_CONTENT) #ResponseBody void updateBoard(
#RequestBody #Valid Board board) throws IllegalArgumentException, MalformedURLException {
boardService.updateUserBoard(board);
}
}
User input:
{
"quantity": "abcdef"
}
As you can see I am using Javax validation annotations #Digits & #Valid but no use because Jackson Deserialization fails while parsing the Integer field quantity.
Is there anyway you can help me to solve this situation by handling this use-case using Javax validation annotations? I reckon changing field type to String in POJO class is an expensive effort as we have to do String to Integer conversion everytime I need some business logic on that field, therefore that is not an option for me.
Well in your case the problem is that you're not conforming to the REST API exposed and try to send a string instead of a number for the quantity field. This should never happen either by you or a third party service that uses your API.
Is there anyway you can help me to solve this situation by handling this use-case using Javax validation annotations?
In any case, if you still want to fix the above, a solution will be to change "quantity" in a string type field and and added pattern matching for it
#Pattern(message = "quantity must be a number", regexp="^[0-9]*$")
private String quantity;
I want to validate my request body with #Valid annotation, but it's not working in Spring Boot
I have a Request class within JAR file which I can't modify with two fields. One field is of type Object. My controller class accept this class object as a request body. When I pass my below JSON to the controller, validation is not working. Below are code samples.
Request Class:
public class Request {
Object data;
Map<String, Object> meta;
public <T> T getData() throws ClassCastException {
return (T) this.data;
}
}
Another Class:
public class StudentSignUpRequest {
#NotNull(message = "First Name should not be empty")
#Size(max = 64, message = "FirstName should not exceed 64 characters")
private String firstName;
#NotNull(message = "Last Name should not be empty")
#Size(max = 64, message = "LastName should not exceed 64 characters")
private String lastName;
#NotNull(message = "Email cannot be empty")
#Size(max = 50, message = "Email cannot exceed 50 characters")
#Pattern(regexp = EMAIL_REGEX_PATTERN, message = "Email should contain a valid email address.")
private String email;
// other fields
}
Controller Class:
#PostMapping(value = Constants.STUDENT_SIGN_UP)
public Response signUpStudent(#Valid #RequestBody Request request, HttpServletRequest servletRequest) {
// retrieving the actual resource from request payload
StudentSignUpRequest signUpRequest = request.getData(StudentSignUpRequest.class);
// call service to sign-up student
return loginRegistrationService.signUpStudent(signUpRequest);
}
Calling code sets request as below:
StudentSignUpRequest studentSignUpRequest = new StudentSignUpRequest();
//setter methods
Request payload = new Request();
payload.setData(studentSignUpRequest);
This is the request I am sending:
For more than 64 chars for firstName:
Sample JSON:
{
"data": {
"firstName": "student111111111111111111111111111111111111111111111111111111111111",
"lastName": "somesurname",
"email": "developer#gmail.com"
}
}
Where first name not included:
{
"data": {
"lastName": "somesurname",
"email": "developer#gmail.com"
}
}
Here both #Size as well as #NotNull annotation not working.
Any solution?
Validation would've worked if the Request class was like;
public class Request {
#Valid
StudentSignUpRequest data;
// other stuff
}
The fact that you have no class type for data makes it impossible for validation to be applied on it, ignoring the fact that there isn't even a #Valid annotation on the field. The #Valid annotation is used to propagate the validation cascade.
But since you cannot modify Request object, let's continue with another way to handle validation without doing it manually.
Another way is to trigger validation after you get the StudentSignUpRequest from request object;
StudentSignUpRequest signUpRequest = request.getData(StudentSignUpRequest.class);
loginRegistrationService.signUpStudent(signUpRequest) // validation will trigger with this call
What you can do is as follows;
#Service
#Validated
public class LoginRegistrationService {
public void signUpStudent(#Valid StudentSignUpRequest signUpRequest) {
// some logic
}
}
with #Validated annotation, you will activate the validation check for any #Valid annotated args in public methods within that class.
Can be used with method level validation, indicating that a
specific class is supposed to be validated at the method level (acting
as a pointcut for the corresponding validation interceptor)
This can be costly since you'd want to get any constraint violation as soon as possible without doing any costly jobs for an already doomed request.
No validation will work the way you are using it, you need to put #valid on the object inside your request object, but since you dont have control on that class the other way around is extend Request object and override getData method and apply #valid on that method, it should work that way.
First use #NotEmpty, #Notblank for Strings. Then ensure you import javax.validation.constraints not that of hibernate. If you are using a custom validator, you will need (final BindingResult bindingResult) as part of your controller method variable.
So you can use below code for validating the same.
public <T> T getData() throws ClassCastException, SomeCustomValidationException {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
Set s = validator.validate(this.data);
//throw SomeCustomValidationException if set is not empty else return this.data
}
A couple of things here:
The type Object for data in Request class makes it impossible for the validator to know that it is of type StudentSignUpRequest. So change the data type.
public class Request {
StudentSignUpRequest data;
Map<String, Object> meta;
}
Secondly, though you have added #Valid in the controller method, in order to validate fields in StudentSignUpRequest you have to add #Valid here as well. Now, data will be validated if passed in the API request. In case it is absent validation won't take place. If you want to make data to be mandatorily passed add #NotNull as well.
public class Request {
#Valid
#NotNull
StudentSignUpRequest data;
Map<String, Object> meta;
}
did you add following dependency?
spring-boot-starter-validation
also check https://www.baeldung.com/spring-boot-bean-validation