Which is the best way to capture a exception in spring mvc. I am not getting the end to end implementation of the exception handling in spring mvc.
I have implemented #ControllerAdvice. Can you please confirm whether I have implemented in right way to handle the exception.
Question:
How can I handle the service layer exception. Do I have to throw it to controller and then to UI? How it works.
How I can handle the sql exception in DAO layer and other exception like Numberformat exception?
Code:
#RequestMapping(value = "/getDepositSearch", method = RequestMethod.POST)
public String depositNumberData(
#ModelAttribute("searchCondition") String searchCondition,
#ModelAttribute("searchText") String searchText,
final RedirectAttributes redirect, Model depositStatus,
HttpServletRequest request) {
String pageForward = null;
try {
List<MRPSDeposit> depositDetails = null;
if (!searchText.isEmpty()) {
depositDetails = mrpsDeposit.getDepositDetails(searchCondition,
searchText);
}
Map<String, String> searchList = new LinkedHashMap<String, String>();
if (searchCondition.equals(ManagementConstants.DEPOSITDATEKEY)) {
searchList.put(ManagementConstants.DEPOSITDATEKEY,
ManagementConstants.DEPOSITDATEVALUE);
} else if (searchCondition.equals(ManagementConstants.DEPOSITNUMBERKEY)) {
searchList.put(ManagementConstants.DEPOSITNUMBERKEY,
ManagementConstants.DEPOSITNUMBERVALUE);
} else {
searchList.put(ManagementConstants.DEPOSITNUMBERKEY,
ManagementConstants.DEPOSITNUMBERVALUE);
searchList.put(ManagementConstants.DEPOSITDATEKEY,
ManagementConstants.DEPOSITDATEVALUE);
}
if (depositDetails.size() == 0) {
redirect.addFlashAttribute("flashMessage",
ManagementConstants.NORECORDFOUND);
pageForward = "redirect:/mrps/getDepositDetails";
} else if (depositDetails.size() > 1) {
Map<String, Map<String, String>> search = new HashMap<String, Map<String, String>>();
search.put("searchContent", searchList);
depositStatus.addAttribute("searchAllContents", search);
depositStatus.addAttribute("depositDetails", depositDetails);
pageForward = "multipleDepositDetails";
} else {
Map<String, Map<String, String>> search = new HashMap<String, Map<String, String>>();
search.put("searchContent", searchList);
depositStatus.addAttribute("searchAllContents", search);
depositStatus.addAttribute("depositDetails", depositDetails);
if (request.isUserInRole("ROLE_READ")) {
pageForward = "readDepositDetails";
} else {
pageForward = "updateDepositDetails";
}
}
} catch (InfoManagementException e) {
System.out.println("weee"+e);
}
return pageForward;
}
Service layer:
#Override
#Transactional(readOnly = true)
public List<MRPSDeposit> getDepositDetails(String searchCondition,
String searchText) {
List<MRPSDeposit> mrpsDepositDetails = new ArrayList<MRPSDeposit>();
/* try { */
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd",
Locale.ENGLISH);
if (searchCondition.equalsIgnoreCase(ManagementConstants.DEPOSITNUMBERKEY)) {
System.out.println("finalal");
mrpsDepositDetails = mrpsDepositDao.findByDepositNumber(
searchCondition, Short.valueOf(searchText));
} else {
try {
mrpsDepositDetails = mrpsDepositDao.findByDepositDate(
searchCondition, formatter.parse(searchText));
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return mrpsDepositDetails;
}
DAO layer:
#Override
public List<MRPSDeposit> findByDepositNumber(String searchCondition,
Short searchTxt) {
List<MRPSDeposit> searchResult = super.findByDepositNumber(
searchCondition, searchTxt);
return searchResult;
}
Controller Advice:
#ControllerAdvice
public class GlobalExceptionController {
#ExceptionHandler(InfoManagementException.class)
public ModelAndView handleCustomException(InfoManagementException ex) {
ModelAndView model = new ModelAndView("error/generic_error");
System.out.println();
model.addObject("errCode", ex.getErrCode());
model.addObject("errMsg", ex.getErrMsg());
return model;
}
#ExceptionHandler(Exception.class)
public ModelAndView handleAllException(Exception ex) {
ModelAndView model = new ModelAndView("error/generic_error");
model.addObject("errMsg", "this is Exception.class");
return model;
}
}
How can I handle the service layer exception. Do I have to throw it to
controller and then to UI? How it works.
In both cases that depends on what are your requirements. There are cases when you will need to show the same page with an error message, also there are some cases when you need to redirect to another error page. In other cases probably you don't need to show any error message.
It's common to throw the exception to the controller then process it in the controller advice and show an readable error message on the UI. In the controller advice you can determine the page that will show the message and also log the error message.
How I can handle the sql
exception in DAO layer and other exception like Numberformat
exception?
I would recommed you to use input validation in the controller. If you use it, then you won't get this kind of error. But if you don't have input validation you can throw the exception and show a message on the UI.
Update
You can leave your service layer as you have at this moment and process the exception in the ControllerAdvice. If you want to process the exception in the service layer you can do this with a try/catch.
public void myServiceMethod(){
try{
...
}catch(Exception1 e){//Every catch block can capture a group of exceptions.
//Depending on your business logic, you can throw a new Exception, log it, or do some logic.
logger.log("My error: ", e);
}catch(Exception2 e){//Every catch block can capture a group of exceptions.
throw new MyBusinessException("Something ocurred", e);
}
}
Then in your ControllerAdvice you need to process MyBusinessException and do what you need.
Related
I have an API-controller that calls a service class. Inside the service class I want to throw an exception so the API-controller can catch it, and return a Http-BadRequest response.
But what exception is equal to Bad Request? And what is best practise for this situation?
I used this pattern for throwing exceptions in the application layer and the api layer would recognize the http status code:
The exceptions definition:
public class BadRequestException : Exception
{
public BadRequestException(string message = null)
: base(message == null ? "Bad Request" : message)
{ }
}
public class ActionInputIsNotValidException : BadRequestException
{
public ActionInputIsNotValidException()
: base("Action input is not valid")
{ }
}
An Action Filter to handle exceptions in api layer:
public class ExceptionActionFilter : ExceptionFilterAttribute
{
public ExceptionActionFilter()
{
}
public override void OnException(ExceptionContext context)
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.HttpContext.Response.ContentType = "application/json";
if (isTypeOf(context.Exception, typeof(Exceptions.BadRequestException)))
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
context.Result = new JsonResult(new
{
Message = context.Exception.Message,
});
}
private bool isTypeOf(Exception exception, Type baseType)
{
return exception.GetType() == baseType || exception.GetType().IsSubclassOf(baseType);
}
}
Then in the application layer we can throw exceptions and the result of api call will be a json containing error message with http 400 status code:
throw new ActionInputIsNotValidException();
I created a form where the user can update his data account. In this form the user is also able to change the account password, before doing so, I ask him the current password, this is the field:
<div class="form-group">
<label>Current Password</label>
<input class="form-control" id="oldPassword"
asp-for="#Model.ExistingPassword" type="password" />
<div class="invalid-feedback"></div>
</div>
as you can see the oldPassword input bound the property ExistingPassword which is part of the ViewModel of that View and have the following declaration:
[Required, MinLength(6), MaxLength(50), DataType(DataType.Password)]
public string ExistingPassword { get; set; }
when the form is submitted I call the following ajax function:
$.post(url, user, function (response) {
//Some stuff
}).done(function (response) {
alert("Updated executed");
}).fail(function (jqXHR, textStatus, errorThrown) {
alert("Error happened!");
});
the parameter of the function are taken by the form, in particular:
url: $(this).attr('action');
user: $(this).serialize();
the action of the form will call the following controller: User\UpdateUser.
Inside the UpdateUser method I execute the following check:
public async Task<UserProfileViewModel> UpdateUserAsync(UserProfileViewModel updatedUser)
{
if (!await _userManager.CheckPasswordAsync(originalUser, updatedUser.ExistingPassword))
throw new Exception("Invalid password");
essentially, the condition check if the current password is correct, if not, then an exception will raised.
Now, my question with this is: how can I know which type of exception the method has generated?
I need to know which type of exception the method UpdateUser has generated because there are different exceptions in the method.
Suppose the Invalid Password exceptions is raised, I need to display a message inside invalid-feedback div, next to oldPassword, so the user know why the update has failed.
Thanks in advance for any help.
Normally, I recommend not using an exception except in actual exception circumstances, but given the way you've designed this, you have a few options.
I'd suggest creating a custom "UpdateUserException" that you can throw that will include additional information, which can be provided by an enum or just string.
public class UpdateUserException : Exception {
public UpdateUserError ErrorCondition;
public UpdateUserException(UpdateUserError error, string message)
{
ErrorCondition = error;
Message = message;
}
}
then you would throw it
throw new UpdateUserException(UpdateUserError.BadPassword, "Invalid Password");
then you would catch it
try {}
catch (UpdateUserException e)
{
if (e.ErrorCondition == UpdateUserException.BadPassword)
{
// handle your exception.
}
}
Have a look at the UserManager ChangePassword Method.
You can bind the UserManager to use DependencyInjection like this (in Startup.cs)
public async void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseAuthentication();
app.UseMvc();
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
UserManager<User> userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
}
}
And then in your Controller s Constructor
private readonly UserManager<User> _userManager;
public AccountController(UserManager<User> userManager)
{
_userManager = userManager;
}
And finally your endpoint:
[HttpPost("ChangePassword")]
public async Task<IActionResult> ChangePassword([FromBody]ChangePasswordRequest changePasswordParams)
{
if (changePasswordParams == null)
return BadRequest($"{nameof(changePasswordParams)} must not be null!");
if (string.IsNullOrWhiteSpace(changePasswordParams.OldPassword) || string.IsNullOrWhiteSpace(changePasswordParams.NewPassword))
return BadRequest("old and new passwords have to be provided, but they both are empty.");
var userId = User.Claims.FirstOrDefault(c => c.Type == "id")?.Value;
var user = await _userManager.FindByIdAsync(userId);
var result = await _userManager.ChangePasswordAsync(user, changePasswordParams.OldPassword, changePasswordParams.NewPassword);
if (result.Succeeded)
return NoContent();
return BadRequest(result.Errors);
}
after that you can handle the errors in a switch statement.
Using Exceptions for handled errors are not recommended since they generally ends up with Internal Server error and actually It is beyond of its purpose.
The best approach would be to send BadRequest as It is stated by #maerlin.
However, If you insist to use Exceptions in your application or your applciation is architected to work in this way. I suggest you to inherit new CustomApplcationException class from ApplicationException and then inherit UpdateUserException and vs. from CustomApplicationException class. After that, I Suggest you to handle your exceptions in ErrorHandlingMiddleware and return HandledExceptions at least with BadRequest (400) status code.
The Example Code would be
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILoggerManager _logger;
public ExceptionMiddleware(RequestDelegate next, ILoggerManager logger)
{
_logger = logger;
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (CustomApplicationException cae)
{
await HandleCustomExceptionAsync(httpContext, cae);
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong: {ex}");
await HandleExceptionAsync(httpContext, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = "Internal Server Error from the custom middleware."
}.ToString());
}
private static Task HandleCustomExceptionAsync(HttpContext context, Exception exception)
{
context.Response.StatusCode = 400;
return context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = exception.Message
}.ToString());
}
}
then you need to regiter middleware in your Startup.cs
app.UseMiddleware<ExceptionMiddleware>();
please see https://code-maze.com/global-error-handling-aspnetcore/ and http://www.talkingdotnet.com/global-exception-handling-in-aspnet-core-webapi/ for further details.
A simple Spring Boot REST Controller
#PostMapping(path = "check-and-submit", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<MyOutput> checkAndSave(#RequestBody #Valid MyInput input, Errors errors){
ResponseEntity<MyOutput> result = null;
if (errors.hasErrors()) {
result = new ResponseEntity<>(MyOutput.buildErrorResponse(errors), HttpStatus.INTERNAL_SERVER_ERROR);
} else {
myDao.save(input.buildEntity());
result = new ResponseEntity<>(MyOutput.buildSuccessResponse(), HttpStatus.OK);
}
return result;
}
And the test class for it
public static void main(String[] args) {
MyInput dto = new MyInput();
// set properties
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
headers.add("Content-Type", "application/json");
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
HttpEntity<MyInput> request = new HttpEntity<MyInput>(dto, headers);
try {
ResponseEntity<MyOutput> result = restTemplate.postForEntity(URL, request, MyOutput.class);
System.out.println(result);
} catch(Exception e) {
e.printStackTrace();
}
}
For success scenario this works fine. But, for exception scenrio, i.e. HTTP 500 this fails
org.springframework.web.client.HttpServerErrorException: 500 null
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:97)
As suggested in one of the posts, I created a error-handler that can successfully read the response
public class TestHandler extends DefaultResponseErrorHandler {
#Override
public void handleError(ClientHttpResponse response) throws IOException {
Scanner scanner = new Scanner(response.getBody());
String data = "";
while (scanner.hasNext())
data += scanner.next();
System.out.println(data);
scanner.close();
}
}
But how can I let RestTemplate read and deserialize the response JSON even in case of HTTP 500.
Before any other human-question-flagging-bot marks this as duplicate, here's a humble explanation on how this is different from the others.
All other questions address how to handle HTTP 500, at max read the response-body. This questions is directed at if it is possible to deserialize the response as JSON as well. Such functionality is well established in frameworks such as JBoss RESTEasy. Checking how same can be achieved in Spring.
This should work.
try {
ResponseEntity<MyOutput> result = restTemplate.postForEntity(URL, request, MyOutput.class);
} catch(HttpServerErrorException errorException) {
String responseBody = errorException.getResponseBodyAsString();
// You can use this string to create MyOutput pojo using ObjectMapper.
}
I implemented a custom controller factory in ASP.NET MVC, and I registered it in global.ascx. The idea is to handle the case of 404 and also exceptions in the controller constructors. I know the factory has been assigned to ASP.NET MVC, because on requests, I can step into it. I can see that I'm returning the controller that I think. But why, oh why on earth, is not my controller used? But I'd think I'd get the usual action not found exception, not controller..conceptually I'm wondering if this is even the right spot to do this in.
protected override IController GetControllerInstance
(RequestContext context,
Type controllerType)
{
IController controller = null;
try
{
controller = base.GetControllerInstance(context, controllerType);
}
catch (CurrentSessionException)
{
controller = new LoginController();
}
catch (System.Web.HttpException)
{
controller = new ErrorController();
}
catch (System.Exception)
{
controller = new ErrorController();
}
return controller;
}
Try manually clearing the errors in your catch statement.
requestContext.HttpContext.ClearError();
Ideally this is best handled as a Filter. MVC comes with a HandleErrorAttribute which you can subclass. You would override the OnException method and then simple handle the logic as you wish.
This is what MVC 3 does by default.
public virtual void OnException(ExceptionContext filterContext) {
if (filterContext == null) {
throw new ArgumentNullException("filterContext");
}
if (filterContext.IsChildAction) {
return;
}
// If custom errors are disabled, we need to let the normal ASP.NET exception handler
// execute so that the user can see useful debugging information.
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled) {
return;
}
Exception exception = filterContext.Exception;
// If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
// ignore it.
if (new HttpException(null, exception).GetHttpCode() != 500) {
return;
}
if (!ExceptionType.IsInstanceOfType(exception)) {
return;
}
string controllerName = (string)filterContext.RouteData.Values["controller"];
string actionName = (string)filterContext.RouteData.Values["action"];
HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
filterContext.Result = new ViewResult {
ViewName = View,
MasterName = Master,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
// Certain versions of IIS will sometimes use their own error page when
// they detect a server error. Setting this property indicates that we
// want it to try to render ASP.NET MVC's error page instead.
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
How can I intercept and send custom error messages with file upload when file size is exceeded. I have an annotated exception handler in the controller class, but the request does not come to the controller. The answer I came across on this link How to handle MaxUploadSizeExceededException suggests implementing HandlerExceptionResolver.
Have things changed in Spring 3.5 or is that still the only solution?
I ended up implementing HandlerExceptionResolver:
#Component public class ExceptionResolverImpl implements HandlerExceptionResolver {
private static final Logger LOG = LoggerFactory.getLogger(ExceptionResolverImpl.class);
#Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object obj, Exception exc) {
if(exc instanceof MaxUploadSizeExceededException) {
response.setContentType("text/html");
response.setStatus(HttpStatus.REQUEST_ENTITY_TOO_LARGE.value());
try {
PrintWriter out = response.getWriter();
Long maxSizeInBytes = ((MaxUploadSizeExceededException) exc).getMaxUploadSize();
String message = "Maximum upload size of " + maxSizeInBytes + " Bytes per attachment exceeded";
//send json response
JSONObject json = new JSONObject();
json.put(REConstants.JSON_KEY_MESSAGE, message);
json.put(REConstants.JSON_KEY_SUCCESS, false);
String body = json.toString();
out.println("<html><body><textarea>" + body + "</textarea></body></html>");
return new ModelAndView();
}
catch (IOException e) {
LOG.error("Error writing to output stream", e);
}
}
//for default behaviour
return null;
}
}