How to correctly handle exceptions from the service (spring boot rest) - spring-boot

When building a rest api using spring boot what is the best way to handle exceptions from the service level and pass them to the controller, so the client gets a custom json error message.
{
"message": "some error"
}
Endpoint from controller
#PostMapping("/login")
public String login(#RequestBody #Valid LoginDto loginDto) {
return gson.toJson(userService.login(loginDto.getUsername(), loginDto.getPassword()));
}
Service level code
public LoginResponseDto login(String username, String password) {
try {
//performs some checks
...
return new LoginResponseDto(token.get());
} catch (AuthenticationException e){
LOGGER.info("Log in failed for user {}", username);
}
return new LoginResponseDto("login failed");
}
LoginResponseDto class
String token;
String message;
public LoginResponseDto(String message) {
this.message = message;
}
Currently it is obviously returning the correctly message but not the correct status code, it will show status 200 with the error message in json.

You have some options:
1) Returning a message:
If you want to return a message something like this,
{
"message": "some error"
}
What you can do is:
Option 1: Create a custom POJO class for error message and return the reference to the object of that POJO class.
Something like this:
ErrorMessage.java
package org.example;
public class ErrorMessage {
private String message;
public ErrorMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Request Handler Method in Controller:
#GetMapping("/login{?username, password}")
public ErrorMessage isUserAuthenticated(#RequestParam String username, #RequestParam String password) {
if (username.toLowerCase().contentEquals("root") && password.contentEquals("system")) {
return new ErrorMessage("authenticated");
}
return null;
}
Option 2: Create a Map and insert key-value pairs that you want to have in the message.
Like this:
#GetMapping("/login{?username, password}")
public Map<String, String> isUserAuthenticated(#RequestParam String username, #RequestParam String password) {
Map<String, String> message = new HashMap<>();
if (username.toLowerCase().contentEquals("root") && password.contentEquals("system")) {
message.put("message", "authenticated");
}
return message;
}
2) Returning an error status code (highly recommended by me):
You may use ResponseEntity for this purpose.
#GetMapping("/login{?username, password}")
public ResponseEntity<?> isUserAuthenticated(#RequestParam String username, #RequestParam String password) {
if (username.toLowerCase().contentEquals("root") && password.contentEquals("system")) {
return new ResponseEntity<>(HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}

Related

spring resttemplate request object not mapping to rest controller

i have below resttempalte which invokes rest controller of another service..
#Override
public ResponseEntity<String> callRestAPI(APIReqDataMO apiReqDataMO) {
String apiURL = URIGenerator.getAPIURL(apiReqDataMO);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Accept", MediaType.APPLICATION_JSON_VALUE);
HttpEntity<?> request = new HttpEntity<>(apiReqDataMO.getRequestObject(), headers);
ResponseEntity<String> httpRes = restTemplate.postForEntity(apiURL, request, String.class);
return httpRes;
}
and in my service i have controller, which consumes above request..
#RequestMapping(value = "/targetService/createUser", method = RequestMethod.POST, consumes = "application/json")
public String fuzzerServiceAge(UserMO userMO) {
System.out.println("---------------------age is -------------------------" + userMO.getAge());
if (userMO.getAge() > 0) {
System.out.println("error age greater than 0 ");
return "invalid user age";
} else if (userMO.getAge() == 0) {
return "invalid user age";
}
return "user added successfully";
}
when i try my test.. the age which i am pushing through rest template is not getting mapped..and i am getting age as 0 always in my system.out.. what could be wrong in my code... and is there anything missing from configuration perspective..
EDIT -
public class APIReqDataMO {
private String restAPIURL;
private Object[] pathParam;
private Object[] requestParam;
private String requestType;
private String paramType;
private Object requestObject;
public String getParamType() {
return paramType;
}
public void setParamType(String paramType) {
this.paramType = paramType;
}
public String getRequestType() {
return requestType;
}
public void setRequestType(String requestType) {
this.requestType = requestType;
}
public Object getRequestObject() {
return requestObject;
}
public void setRequestObject(Object requestObject) {
this.requestObject = requestObject;
}
public String getRestAPIURL() {
return restAPIURL;
}
public void setRestAPIURL(String restAPIURL) {
this.restAPIURL = restAPIURL;
}
public Object[] getPathParam() {
return pathParam;
}
public void setPathParam(Object[] pathParam) {
this.pathParam = pathParam;
}
public Object[] getRequestParam() {
return requestParam;
}
public void setRequestParam(Object[] requestParam) {
this.requestParam = requestParam;
}
}
controller
#PostMapping("/targetService/createUser")
public String fuzzerServiceAge(UserMO userMO) {
System.out.println("--------------------- age is -------------------------" + userMO.getAge());
if (userMO.getAge() > 0) {
// return ResponseEntity.ok("Hello World!");
} else if (userMO.getAge() == 0) {
System.out.println(" it is else block");
// return ResponseEntity.badRequest().build();
}
// return ResponseEntity.ok("user added successfully!");
return "user added successfully";
}
usermo
public class UserMO {
#JsonProperty("name")
private String name;
#JsonProperty("age")
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Issue
There is an issue in API implementation. You are creating POST API and when the user will invoke this API by passing UserMO in the request body then mapping won't happen because the #RequestBody annotation is missing.
#PostMapping("/targetService/createUser")
public String fuzzerServiceAge(UserMO userMO) {
System.out.println("--------------------- age is -------------------------" + userMO.getAge());
if (userMO.getAge() > 0) {
// return ResponseEntity.ok("Hello World!");
} else if (userMO.getAge() == 0) {
System.out.println(" it is else block");
// return ResponseEntity.badRequest().build();
}
// return ResponseEntity.ok("user added successfully!");
return "user added successfully";
}
Solution
If you are using #RestController annotation on top of the controller class then add #RequestBody annotation before UserMO userMO and try again.
Like this
#PostMapping("/targetService/createUser")
public String fuzzerServiceAge(#RequestBody UserMO userMO) {
//logic
}
if you are using #Controller annotation on top of the controller class then add #ResponseBody annotation on top of method fuzzerServiceAge() and #RequestBody annotation before UserMO userMO and try again.
Like this
#PostMapping("/targetService/createUser")
#ResponseBody
public String fuzzerServiceAge(#RequestBody UserMO userMO) {
//logic
}

What is the best way to return different types of ResponseEntity in Spring-Boot

I would like to return two different response for a spring boot rest API.
I should not be using <?> wild card as i get the sonar issue "Generic wildcard types should not be used in return types"
My code:
#GetMapping(path = {"/v1/{type}"}, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> method(#PathVariable(value = "type") boolean type) {
boolean b = type;// some logic
if (b) {
Success result=new Success();
result.setSuccess("Yes");
return new ResponseEntity<>(result,HttpStatus.OK);
}
else {
Error result=new Error();
result.setError("No");
return new ResponseEntity<>(result,HttpStatus.CONFLICT); //appropriate error code
}
}
Any idea how to handle this situation.
Update:
public interface MyResponse{
public Success getSuccessObj();
public Error getErrorObj();
}
#Service
public class Success implements MyResponse {
public Error getErrorObj(){
return null;
}
public Success getSuccessObj(){
Success s=new Success();
return s;
}
#Service
public class Error implements MyResponse {
public Error getErrorObj(){
Error e=new Error();
return e;
}
public Success getSuccessObj(){
return null;
}
Not claiming to be "the best way", but one approach can be:
Introduce:
package com.my.package;
public interface MyResponseI { //if Error, Success (and others) have more "in common", you can also introduce an (abstract) class (with fields, methods, etc.)!
}
"Implement"/Extend:
public class Success implements com.my.package.MyResponseI { //everything else can stay}
as
public class Error implements com.my.package.MyResponseI { //everything else can stay}
Use as Response Type:
#...
public ResponseEntity<com.my.package.MyResponseI> ...
(on client side distinguish).
..and in "your domain" (error, success, ...), you are free to use any "tweaks" of a object oriented design.
Useful links/entries:
https://stackoverflow.blog/2020/03/02/best-practices-for-rest-api-design/
https://swagger.io/resources/articles/best-practices-in-api-design/
https://www.google.com/search?q=rest+api+design
, but also
https://www.google.com/search?q=object+oriented+design
and https://www.google.com/search?q=domain+driven+design
This should work
I tried the snippet below by myself and it worked for me:
#GetMapping("/testresponse/{id}")
public ResponseEntity<?> testMyResponse(#PathVariable("id") int id)
{
if(id==1)
return ResponseEntity.ok(new Success());
else return new ResponseEntity<>(new Error(), HttpStatus.CONFLICT);
}
public class Success {
private String msg = "Success";
public String getMsg() {
return msg;
}
}
public class Error {
private String msg = "Error";
public String getMsg() {
return msg;
}
}
EDIT: The solution as below doesn't work
You should also define an interface for both Success and Error classes. Let say the interface MyResponse
And then change your method declaration, it would look like this
public ResponseEntity<MyResponse> method(#PathVariable(value = "type") boolean type)
If so, the return statement, could be:
return new ResponseEntity<>(result, HttpStatus.OK);
Or
//for status 200 OK
return ResponseEntity.ok(result);

Default return error for white label erorr

I have this controller which is open when user is redirected.
#Controller()
public class RedirectController {
#RequestMapping(method = RequestMethod.GET, value = "/redirect")
#ResponseBody
public String handleRedirectMessage(HttpServletRequest request) throws Exception {
......
}
}
But when the match is not found I get Whitelabel Error Page. Is there some way to set default handler which is by default open when path is not correct?
You need add following property:
server.error.whitelabel.enabled=false
And implement org.springframework.boot.web.servlet.error.ErrorController interface and overrides its getErrorPath() to return a custom path to call when an error occurred:
#Controller
public class MyErrorController implements ErrorController {
#RequestMapping("/error")
public String handleError() {
//do something like logging
return "error";
}
#Override
public String getErrorPath() {
return "/error";
}
}
After that add error page for path "/error" Thymeleaf error.html.
Also you could add pager for different errors:
#RequestMapping("/error")
public String handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (status != null) {
Integer statusCode = Integer.valueOf(status.toString());
if(statusCode == HttpStatus.NOT_FOUND.value()) {
return "error-404";
}
else if(statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
return "error-500";
}
}
return "error";
}
You can find here full code example
and description.

Spring throwing 403 exception on POST request but POSTMAN request working

I am trying to POST some data to rest api, When I send the request to API using SPRING REST I get the 403 exception.
I have tried adding user-agent header as suggested by other answers but nothing has worked for me so far. I also checked that access key when using POSTMAN and when calling the service is same. Any advice would be helpful;
The wrapper class to create the body of POST request
public class ApiRequest implements Serializable {
private static final long serialVersionUID = 3729607216939594972L;
#JsonProperty("id")
List<Integer> id;
#JsonProperty("sdate")
String sdate;
#JsonProperty("edate")
String edate;
#JsonProperty("fields")
List<String> fields;
public ApiRequest(List<Integer> id, String sdate, String edate, List<String> fields){
this.id=id;
this.sdate=sdate;
this.edate=edate;
this.fields=fields;
}
public void setEdate(String edate) {
this.edate = edate;
}
public void setSdate(String sdate){
this.sdate=sdate;
}
public void setFields(List<String> fields) {
this.fields = fields;
}
public void setId(List<Integer> id) {
this.id = id;
}
public String getEdate() {
return edate;
}
public String getSdate() {
return sdate;
}
public List<String> getFields() {
return fields;
}
public List<Integer> getId() {
return id;
}
#Override
public String toString() {
return "ApiRequest{" +
"id=" + id +
", sdate=" + sdate +
", edate=" + edate +
", fields=" + fields+
'}';
}
}
Code to call the api
private HttpHeaders getRequestHeaders() {
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setContentType(MediaType.APPLICATION_JSON);
requestHeaders.setAccept(Arrays.asList(MediaType.ALL));
requestHeaders.set("user-agent","Some User Agent);
requestHeaders.set("access_token", "ACCESS_TOKEN");
return requestHeaders;
}
ApiRequest request=new ApiRequest(Arrays.asList(10),DateUtil.today().toString(),DateUtil.today().plusDays(10).toString(),Arrays.asList("ALL"));
String response=post("RANDOM_URL",null,null,request,getRequestHeaders(),String.class,"");
Post super method:
public <T> T post(String baseUrl, String url, String query, Object body, HttpHeaders requestHeaders, Class<T> responseClassType, String logTag) {
// In this method body is converted to Json String and called the restExchange
If you are sure that with Postman you are getting correct results then you can enable debug logs for the underlying httpclient ( if apache http client is the underlying http library) by setting logging.level.org.apache.http=DEBUG. This will print all the request details like url, headers etc by which you can compare with what you are sending with Postman. If the client library is something different then you may need to write an interceptor to capture all the request details as explained here.

Spring MVC ExceptionHandling: action annotated as #ExceptionHandling can't pass variable to error view

I know a lot of people have had issues similar to this.Sorry posting it again, but i believe there is something i might not be doing well.
I'm using Spring 3.0.5 with freemarker 2.3.14. Basically i wanted to show a friendly error message to the user.
#Controller("exceptioncontroller")
public class ExceptionController {
private static Logger logger = Logger.getLogger(ExceptionController.class);
#RequestMapping(value = "/site/contentnofoundexception")
public String throwContentFileNotFound(){
boolean exception = true;
if(exception){
throw new ContentFileNotFoundException("content ZZZ123 not found");
}
return "errortest";
}
#ExceptionHandler(value = ContentFileNotFoundException.class)
public String handleFileNotFoundException(ContentFileNotFoundException ex, Model model) {
model.addAttribute("msg",ex.getErrorMessage());//this message is never passed to the error view. msg is always null
return "error";
}
}
//same issue for handleException action which uses ModelAndView
#ExceptionHandler(value = Exception.class)
public ModelAndView handleException(Exception ex){
logger.error(ex);
ModelAndView mv = new ModelAndView();
mv.setViewName("error");
String message = "Something Broke. Please try again later";
mv.addObject("msg", message);
return mv;
}
// Custom Exception class
public class ContentFileNotFoundException extends RuntimeException {
private String errorMessage;
public ContentFileNotFoundException(String message) {
this.setErrorMessage(message);
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}
So each case either handleFileNotFoundException or handleException actions are called alright but they can't send any message to the error.ftl view to display to the user. Is there anything i need to configure?
Thanks for helping in advance

Resources