Multiple error pages from the same ExceptionHandler - spring

In my current spring-boot project, I have this ExceptionHandler:
#ControllerAdvice
public class GlobalDefaultExceptionHandler {
#ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest request, Exception error) throws Exception {
if (AnnotationUtils.findAnnotation(error.getClass(), ResponseStatus.class) != null)
throw error;
ModelAndView mav = new ModelAndView();
mav.setAttribute("error", error);
return mav;
}
}
What I want to do it's make this handler redirect to different error pages, depending of origin the error.
I have two "types" of pages in my project: a public one, acessed both by an anonymous user or an authenticated user, and the admin pages (private), acessed only by authenticated users.
This two types of page have different styles. I want, when an error occurs when the user is in the public pages, an error page with the public style be shown. If the errors occurs when the user is in the private pages, another error page, with the style of the private pages be shown.

You can construct a selection of what exception classes it needs to throw in your controller , Assuming like this:
#RequestMapping(value = "/check")
public ModelAndView processUser( ) throws Exception {
ModelAndView modelAndView = new ModelAndView();
if (something... ) {
throw new GlobalDefaultExceptionHandler( ); // throws GlobalDefaultExceptionHandler
}
if (something else... ) {
throw new AnotherExceptionHandler( );// throws 'anotherExceptionHandler'
}
// If there isn't exception thrown....do something
}
And assuming this is the AnotherExceptionHandler class:
#ControllerAdvice
public class AnotherExceptionHandler{
#ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest request, Exception error) throws Exception {
if (AnnotationUtils.findAnnotation(error.getClass(), ResponseStatus.class) != null)
throw error;
// Go to another view
ModelAndView mav = new ModelAndView();
mav.setAttribute("anotherError", error);
return mav;
}
}
But if you are forced to use only an handler , you can just use selection directly just :
#ControllerAdvice
public class GlobalDefaultExceptionHandler {
#ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest request, Exception error) throws Exception {
ModelAndView mav =null;
if ( // something...){
mav = new ModelAndView()
mav.setAttribute("error", ... );
return mav;
}
else if (// another something...){
mav = new ModelAndView()
mav.setAttribute("anotherError", ...);
return mav;
}
return mav;
}

There are several options. Some of them are:
You can check if user has been authenticated by using request.getUserPrincipal(). The return value will be null if user is not authentcicated. Depending on the result, you can return a different view.
Have all your Contollers service public pages extend from one PublicBaseController and controllers service private pages extend PrivateBaseController. Add method annotated with #ExceptionHandler to base controllers, which return appropriate views.

Related

Spring MVC, redirect to error after Rest call exception

I'm performing a Rest request in one of my controllers and I'd like to redirect to my error view if the request went wrong (404, 503 ...)
My controller calls this function :
public String functionTest(){
String date, res;
String url = "myRestUrl/{param}";
Map<String, String> uriParams = new HashMap<>();
uriParams.put("param", "param");
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(url);
HttpEntity<String> request = new HttpEntity<>(createHeaders());
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new RestErrorHandler());
ResponseEntity<String> response = restTemplate.exchange(builder.buildAndExpand(uriParams).toUri(), HttpMethod.GET, request, String.class);
res= response.getBody().getMyResult();
return res;
}
And here is my Rest error handler :
public class RestErrorHandler extends DefaultResponseErrorHandler {
#Override
public void handleError(ClientHttpResponse response) throws IOException {
//Here I need to use a ModelAndView to redirect to error view but I'm not anymore in my controller
}
}
I guess I'm doing it wrong, do you have any solutions ?
Here is a rather simplistic way to achieve what you need. If you would like more control/flexibility, refer to this Spring blog article.
In your (client) RestErrorHandler:
public class RestErrorHandler extends DefaultResponseErrorHandler {
#Override
public void handleError(ClientHttpResponse response)
throws IOException {
// Do some stuff here ...
// This could be your own exception, for example.
throw new IOException();
}
}
Then in your controller (or refer to the article above if you want other options):
// Your requestMappings here ...
#ExceptionHandler(Exception.class)
public ModelAndView handleError(HttpServletRequest req, Exception ex) {
ModelAndView mav = new ModelAndView();
// Do stuff and redirect to your error view.
return mav;
}
EDIT: Another solution would be to catch Spring's RestClientException, which is a RuntimeException thrown by RestTemplate when it encounters an error:
try {
restTemplate.exchange(...);
} catch (RestClientException ex) {
// Do stuff here ...
}

Spring - Manage custom Exception page

I'm trying to manage a custom error page with my custom exception.
I have this exception
#ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Inesistente")
public class ResourceNotAccessibleException extends Throwable{
public ResourceNotAccessibleException(String message){
super(message);
}
}
which i want to respond with a 404 error.
Than i'm managing an error controller
#ControllerAdvice
public class ErrorController {
#ExceptionHandler({ResourceNotAccessibleException.class})
public ModelAndView getErrorPage(HttpServletRequest request, Throwable ex) {
String errorMsg = "";
int httpErrorCode = getErrorCode(request);
switch (httpErrorCode) {
case 404: {
logger.error("Status Error " + httpErrorCode , ex.getMessage());
errorMsg = messageSource.getMessage("errorMessage", new Object[] { uuid, +httpErrorCode }, locale);
break;
}
case 400: {
errorMsg = "BAD REQUEST";
break;
}
case 500: {
errorMsg = messageSource.getMessage("errorMessage", new Object[] { uuid, +httpErrorCode }, locale);
logger.error("Status Error " + httpErrorCode , ex.getMessage());
break;
}
}
ModelAndView mav = new ModelAndView();
mav.addObject("errorMsg", errorMsg);
mav.setViewName("error");
return mav;
}
Now, in my controller if i have something like
if(object==null) {
throw new ResourceNotAccessibleException("Resource does not exist");
}
I should see my error view, but i'm getting the classic white error page, in my log i see the exception being hit..
The ResourceNotAccessibleException should extend Exception or RuntimeException and not Throwable. More info
If you can't change exception type, probably you could try ExceptionHandlerExceptionResolver or this awesome post about Spring exception handling
One more thing, you probably want to add some #ResponseStatus info above getErrorPage, because you are handling this exeption and #ResponseStatus annotation above the ResourceNotAccessibleException will never trigger.
So i think something like this should work:
#ControllerAdvice
public class ErrorController {
#ResponseStatus(value= HttpStatus.NOT_FOUND) // <= important
#ExceptionHandler({ResourceNotAccessibleException.class})
public ModelAndView getErrorPage(HttpServletRequest request, Throwable ex) {
String errorMsg = "";
// ... some code here
ModelAndView mav = new ModelAndView();
mav.addObject("errorMsg", errorMsg);
mav.setViewName("error");
return mav;
}
}
public class ResourceNotAccessibleException extends Exception{ // <= important
public ResourceNotAccessibleException(String message){
super(message);
}
}
If this doesn't work, you can also try to change resource view file name to something like errorPage.jsp or errorPage.html and set it like mav.setViewName("errorPage");
You need to replace the default error pages in your web container and map a status code to a particular error page.
Here are the changes you need to make:
If it's a Jetty container, here are the changes:
#Bean
public JettyEmbeddedServletContainerFactory
containerFactory(
#Value("${server.port:8080}") final String port,
#Value("${jetty.threadPool.maxThreads:600}") final String maxThreads,
#Value("${jetty.threadPool.minThreads:10}") final String minThreads,
#Value("${jetty.threadPool.idleTimeout:5000}") final String idleTimeout) {
final JettyEmbeddedServletContainerFactory factory =
new JettyEmbeddedServletContainerFactory(Integer.valueOf(port));
...
factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,
"/error-info.html"));
...
return factory;
}
If it's a Tomcat container, here are the changes:
#Bean
public EmbeddedServletContainerCustomizer container() {
return new EmbeddedServletContainerCustomizer() {
#Override
public void customize(
ConfigurableEmbeddedServletContainer container) {
container.addErrorPages(new
ErrorPage(HttpStatus.NOT_FOUND, "/error-info.html"));
}
};
}
For your ErrorController, don't set view name. It will pick the view from the error page mapping which was set earlier.
#ControllerAdvice
public class ErrorController {
#ExceptionHandler(ResourceNotAccessibleException.class)
#ResponseStatus(value = HttpStatus.NOT_FOUND)
public ModelAndView handleResourceNotAccessibleException(
HttpServletRequest req, ResourceNotAccessibleException ex) {
...
ModelAndView mav = new ModelAndView();
mav.addObject("errorMsg", errorMsg);
retrun mav;
}
}
Location of error-info.html or jsp under resources/static

Spring ModelAndView Attribute is null

I have one simple controller and one interceptor.
Within interceptor in postHandle-method I am checking user.
Problem: My user-model is sometimes null, between controller-handles.
postHandle invoked by home-handle==> User-Model is not null
postHandle invoked by check_user-handle ==> User-model is null
postHandle invoked by redirectToErrorPage-handle ==> User-model is
not null anymore and contains everything, what i've expected by
check_user-PostHandle Invocation.
Here is my controller
#Controller
#SessionAttributes("user")
public class HomeController {
#RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView home(Model model, HttpServletRequest request, HttpSession session) {
User user = new User();
return new ModelAndView("login", "user", user);
////now User-Model was saved in the session
}
//now i'am redirectring user in the "check_user"-handle
#RequestMapping(value = "/check_user", method = RequestMethod.POST)
public ModelAndView checkUser(#Valid #ModelAttribute("user") User user, BindingResult bindingResult, Model model,
HttpServletRequest request, RedirectAttributes redirectAttr) {
RedirectView redirectView = null;
ModelAndView mav=null;
try {
if(!bindingResult.hasErrors()){
redirectView = new RedirectView("home");
redirectView.setStatusCode(HttpStatus.FOUND);
redirectAttr.addFlashAttribute("httpStatus", HttpStatus.FOUND);
mav = new ModelAndView(redirectView);
return mav; //at next i entry post-handle from interceptor,
//which says to me, that user-model is null.
}
}
//My interceptor redirects me to this handle.
//After this handle within interceptor method "postHandle", i see, that
//user-object exists
#RequestMapping(value = "/error", method = RequestMethod.GET)
public String redirectToErrorPage(HttpServletRequest request){
return "error";
}
}
And my Interceptor:
public class UserInterceptor extends HandlerInterceptorAdapter {
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
User user = (User) modelAndView.getModel().get("user");
if(user == null || !user.isAdmin()){
response.sendRedirect(request.getContextPath()+"/failed");
}
}
}
While I retrive, which keys my model has, when postHandle was invoked by "check_user", I have only one key "totalTime". Whats going on with my model?
Try modifying your postHandle() as below:
...
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user = (User) request.getSession().getAttribute("user");
...

processFormSubmission not being called

I am new to spring. I am creating a simple login page. But the processFormSubmission() is not being called. But the showForm() is working.
public class LoginController extends SimpleFormController
{
private LoginService loginService;
private String loginView;
public LoginService getLoginService() {
return loginService;
}
public void setLoginService(LoginService loginService) {
this.loginService = loginService;
}
public String getLoginView() {
return loginView;
}
public void setLoginView(String loginView) {
this.loginView = loginView;
}
public LoginController() {
setBindOnNewForm(true);
}
#Override
protected ModelAndView processFormSubmission(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception
{
TraceUser tr = (TraceUser) command;
System.out.println(tr);
//loginService.
return super.processFormSubmission(request, response, command, errors);
}
#Override
protected ModelAndView showForm(HttpServletRequest request,
HttpServletResponse response, BindException errors)
throws Exception {
ModelAndView mav = new ModelAndView();
mav.addObject("traceUser", new TraceUser());
mav.setViewName(getLoginView());
return mav;
}
}
And please help me out with how should the ModelAndView object should be processed further.
First of all, the use of the Controller API has been left aside in favor of the new annotation-based controllers (see the #RequestMapping annotation) and classes like SimpleFormController have been deprecated since quite a while now.
However, to answer your question, I assume your form does not declare method="post" and by default, the SFC will consider only POST requests as form submissions (see the isFormSubmission() method in AbstractFormController). Is this the case ?

Rollback transaction in a Spring #Controller and show exception message in view

I have a Spring MVC controller and when an exception occurs I would like to show the exception message in the view and rollback the open transactions. The view contains a form like this:
<form:form method="POST" modelAttribute="registrationForm">
<form:errors path="*" cssClass="error-message"/>
...
</form:form>
I would like to show the exception message in the view, using the <form:errors ... /> feature. This is my current quite horrible solution:
#RequestMapping(value = "/registration", method = RequestMethod.POST)
public ModelAndView submitForm(#ModelAttribute("registrationForm") RegistrationForm registrationForm,
BindingResult result,
ModelAndView modelAndView,
HttpServletRequest request) throws Exception
{
registrationValidator.validate(registrationForm, result);
if(result.hasErrors())
{
return setupForm(modelAndView, registrationForm);
}
else
{
try
{
// ... Some non-transactional operations...
// The following operation is the only one annotated with #Transactional
// myExampleDao is #Autowired, can throw exception
myExampleDao.createFoo(bar);
// ... Other non-transactional operations...
return new ModelAndView("redirect:successful");
}
catch(Exception e)
{
throw new RegistrationException(e, registrationForm, result);
}
}
}
#ExceptionHandler(value = RegistrationException.class)
public ModelAndView registrationExceptionHandler(RegistrationException e) throws Exception
{
RegistrationForm registrationForm = e.getRegistrationForm();
BindingResult result = e.getBindingResult();
result.reject("exception", e.getMessage());
Map<String, Object> model = result.getModel();
return setupForm(new ModelAndView("registration", model), registrationForm);
}
private ModelAndView setupForm(ModelAndView modelAndView, RegistrationForm registrationForm) throws Exception
{
Map<String,Object> model = modelAndView.getModel();
model.put("currentYear", Calendar.getInstance().get(Calendar.YEAR));
return new ModelAndView("registration", model);
}
The problem I'm facing is that when the exception is thrown, the transaction is not rolled back.
Can anyone help?
Thank you.
Update: slightly changed the question for better understanding
Update: found a quite horrible solution to display the exception message in the view. Still facing the problem with the transaction that is not rolled back when the exception is thrown.
Update: I changed #Transactional to #Transactional(rollbackFor = Exception.class) in the MyExampleDao.createFoo(...) method and now everything is working perfectly. This solution is still ugly IMO, does anyone have a better solution?
Don't have transactions in your controllers. Put them in your service layer.
You can create an abstract controller class that implements exception handling like so (then each separate controller extends obviously) :
public class AbstractCtrl {
#Resource(name = "emailService")
private EmailService emailService;
/*
* Default exception handler, catchs all exceptions, redirects to friendly
* error page and send e-mail does not catch request mapping errors
*/
#ExceptionHandler(Exception.class)
public String myExceptionHandler(final Exception e) {
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
final String strStackTrace = sw.toString(); // stack trace as a string
emailService.sendAlertMail(strStackTrace);
return "exception"; // default friendly excpetion message for user
}
}
But DO NOT put transaction in your controllers, put them in Service layer classes.
No one suggested a better solution than my ugly one. Here is my solution that solved the problem I had:
#RequestMapping(value = "/registration", method = RequestMethod.POST)
public ModelAndView submitForm(#ModelAttribute("registrationForm") RegistrationForm registrationForm,
BindingResult result,
ModelAndView modelAndView,
HttpServletRequest request) throws Exception
{
registrationValidator.validate(registrationForm, result);
if(result.hasErrors())
{
return setupForm(modelAndView, registrationForm);
}
else
{
try
{
// ... Some non-transactional operations...
// The following operation is the only one annotated with #Transactional
// myExampleDao is #Autowired, can throw exception
myExampleDao.createFoo(bar);
// ... Other non-transactional operations...
return new ModelAndView("redirect:successful");
}
catch(Exception e)
{
throw new RegistrationException(e, registrationForm, result);
}
}
}
#ExceptionHandler(value = RegistrationException.class)
public ModelAndView registrationExceptionHandler(RegistrationException e) throws Exception
{
RegistrationForm registrationForm = e.getRegistrationForm();
BindingResult result = e.getBindingResult();
result.reject("exception", e.getMessage());
Map<String, Object> model = result.getModel();
return setupForm(new ModelAndView("registration", model), registrationForm);
}
private ModelAndView setupForm(ModelAndView modelAndView, RegistrationForm registrationForm) throws Exception
{
Map<String,Object> model = modelAndView.getModel();
model.put("currentYear", Calendar.getInstance().get(Calendar.YEAR));
return new ModelAndView("registration", model);
}
The only way you have to manage transaction in a controller scope, open a session from the sessionFactory.
Session session = sessionFactory.openSession();
try{
Transaction tx = session.beginTransaction();
// code
session.save(foo);
tx.commit();
}catch(Exception e){
tx.rollback();
}finally{
try{session.close();}finally{}
}

Resources