Saturday, March 3, 2012

Full ajax exception handler

Whenever some business code throws an unhandled exception, due to some unexpected environmental situation (e.g. DB down), or due to session expiration (ViewExpiredException), or due to some overseen bug (fix it asap!), it usually ends up in a HTTP 500 error page or some exception-specific error page, which you can in any way customize according the standard Servlet API rules by a <error-page> in web.xml as follows:

<error-page>
    <error-code>500</error-code>
    <location>/errors/500.xhtml</location>
</error-page>
<error-page>
    <exception-type>javax.faces.application.ViewExpiredException</exception-type>
    <location>/errors/expired.xhtml</location>
</error-page>

However, the error page does not show up at all whenever the exception occurs during a JSF ajax request. In Mojarra, only when the javax.faces.PROJECT_STAGE is set to Development, a bare JavaScript alert dialogue will show up, with only the exception type and message. This may be helpful for developers and testers during development stage, but this alert does thus not show up in Production project stage. The enduser would not get any feedback if the action was successfully performed or not. This is quite frustrating. Also for the developer.

OmniFaces to the rescue

Ideally, JSF should just show the error page in its entirety. This is possible with a custom ExceptionHandler. The OmniFaces project has recently got such an exception handler, written by yours truly, the FullAjaxExceptionHandler (source code here). All you need to do is to register the FullAjaxExceptionHandlerFactory (source code here) in faces-config.xml as follows:

<factory>
    <exception-handler-factory>
        org.omnifaces.exceptionhandler.FullAjaxExceptionHandlerFactory
    </exception-handler-factory>
</factory>

This exception handler factory will register the FullAjaxExceptionHandler which will handle exceptions on ajax requests.

The exception handler will parse the web.xml to find the error page locations of the HTTP error code 500 and all exception types. You only need to make sure that those locations point each to a Facelets file. The location of the HTTP error code 500 or the exception type java.lang.Throwable is required to have at least a fallback error page if none of the specific exception types are matched.

The exception handler will set all error details in the request scope by the standard servlet error request attributes like as in a normal synchronous HTTP 500 error page response. This way the error pages are fully reuseable for both normal and ajax requests. Finally it will create a new UIViewRoot on the error page location and force a partial render of @all. Here's an extract of relevance from the source code:

// Set the necessary servlet request attributes which a bit decent error page may expect.
final HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
request.setAttribute(ATTRIBUTE_ERROR_EXCEPTION, exception);
request.setAttribute(ATTRIBUTE_ERROR_EXCEPTION_TYPE, exception.getClass());
request.setAttribute(ATTRIBUTE_ERROR_MESSAGE, exception.getMessage());
request.setAttribute(ATTRIBUTE_ERROR_REQUEST_URI, request.getRequestURI());
request.setAttribute(ATTRIBUTE_ERROR_STATUS_CODE, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

// Force JSF to render the error page in its entirety to the ajax response.
context.setViewRoot(context.getApplication().getViewHandler().createView(context, errorPageLocation));
context.getPartialViewContext().setRenderAll(true);
context.renderResponse();

// Prevent some servlet containers from handling the error page itself afterwards. So far Tomcat/JBoss
// are known to do that. It would only result in IllegalStateException "response already committed".
Events.addAfterPhaseListener(PhaseId.RENDER_RESPONSE, new Runnable() {
    @Override
    public void run() {
        request.removeAttribute(ATTRIBUTE_ERROR_EXCEPTION);
    }
});

Note the last part. Tomcat and JBoss seem to automatically trigger the default HTTP 500 error page mechanism after JSF has done its job. It turns out that it was triggered by the presence of the javax.servlet.error.exception request attribute, regardless of if it was been set by response.sendError(). Although that would not harm, the response is namely already committed by JSF, but it would clutter your server logs with an IllegalStateException: response already committed every time when the exception handler does its job. Hence the piece of code which removes the request attribute after the render response phase.

Finally, you could show all error details in the error page the usual way as follows:

<ul>
    <li>Date/time: #{of:formatDate(now, 'yyyy-MM-dd HH:mm:ss')}</li>
    <li>HTTP user agent: #{header['user-agent']}</li>
    <li>Request URI: #{requestScope['javax.servlet.error.request_uri']}</li>
    <li>Status code: #{requestScope['javax.servlet.error.status_code']}</li>
    <li>Exception type: #{requestScope['javax.servlet.error.exception_type']}</li>
    <li>Exception message: #{requestScope['javax.servlet.error.message']}</li>
    <li>Exception stack trace: 
        <pre>#{of:printStackTrace(requestScope['javax.servlet.error.exception'])}</pre>
    </li>
</ul>

When using OmniFaces, the #{of:xxx} functions are available by the http://omnifaces.org/functions namespace. Also, when using OmniFaces the java.util.Date representing the current timestamp is implicitly available by #{now}.

Update: the FullAjaxExceptionHandler can be tried live on the new showcase site!

But it does not work with PrimeFaces actions! (update: from 3.2 on, it will!)

Indeed, PrimeFaces does not support a render/update of @all. Here's a cite of Optimus Prime himself:

PrimeFaces does not support update="@all" because update="@all" is fundamentally wrong.

I agree with him to a certain degree. In case of successful requests, it does indeed not make any sense. You would as good just send a normal/synchronous request instead of an ajax/asynchronous request. But in case of failed requests it would have been very useful. Of course, you could send a redirect instead by ExternalContext#redirect(), that would work perfectly fine, but you would lose all request attributes, including the error details. It is really not preferable to fiddle with the session scope or maybe even the flash scope to get them to show up in the error page.

Fortunately, there's a simple way to get PrimeFaces to support @all. Just add the following piece of JavaScript code to your global JavaScript file which should be loaded after PrimeFaces' own scripts (just referencing it by <h:outputScript> ought to be sufficient):

var originalPrimeFacesAjaxResponseFunction = PrimeFaces.ajax.AjaxResponse;
PrimeFaces.ajax.AjaxResponse = function(responseXML) {
  var newView = $(responseXML.documentElement).find("update[id='javax.faces.ViewRoot']").text();

  if (newView) {
    $('head').html(newView.substring(newView.indexOf("<head>") + 6, newView.indexOf("</head>")));
    $('body').html(newView.substring(newView.indexOf("<body>") + 6, newView.indexOf("</body>")));
  }
  else {
    originalPrimeFacesAjaxResponseFunction.apply(this, arguments);
  }
};

Update: the PrimeFaces support for update="@all" will be available with 3.2, great job, Optimus Prime!

39 comments:

Oleg Varaksin said...

Wow. Nice exception handling of ajax requests. I use similar stuff, but with redirect() and session scoped bean because it doesn't hurt in (rare) case of error, in my opinion. This bean is bound in the error page via preRenderView listener and clears values at the end.

One question yet. Do you plan to extend the FullAjaxExceptionHandler in order to handle 404 and similar errors. 500 is only one possible error. I propose to configure the list of errors to be handled. Along with corresponding error pages. What do you think?

Oleg Varaksin said...

I mean a small session scoped bean transfers error details into request scope bean and clear its values at the end. Request scoped bean is bound in the error page via preRenderView listener.

Oleg Varaksin said...

By the way, PrimeFaces always supported process="@all". Only update="@all" was a problem, but it's fixed for 3.2 release too.

BalusC said...

@Oleg: Unhandled exceptions by default end up in a HTTP 500 error page. HTTP 404 is just a page not found and it is very unlikely that it would ever be caused by ajax requests, page-to-page navigation is normally to be perfomed by synchronous requests.

Yes, I know that PrimeFaces supports process="@all". The problem was in update="@all". Good to know that update="@all" will be in 3.2. Is this mentioned anywhere? I can't find it in its issue list right now.

Oleg Varaksin said...

Yes, it's here http://code.google.com/p/primefaces/issues/detail?id=3643 Thanks to you :-)

I have often faced 404 when resources were not found (by any reason). But you're right, for ajax requests it's a rare case if you don't play with navigation programmatically.

BalusC said...

@Oleg and anyone who is interested: I have just improved the exception handler to support specific exception types as well. So you can also use this for example for the dreaded ViewExpiredException.

Thang Pham said...

Hi balusc. Where you explain about primefaces integration, u said that we redirect, we would lose request attribute, that why your ajax exception handler is better. When u said request attribute, does that mean form attribute, like text that user type in, drop down value that user select. if so, how do redisplay what the user select or type, because after I see the error page, if I click the back button, different browser behave differently. FireFox redisplay what I type or select, ie does not

BalusC said...

@Thang: It was never the intent to retain the original form input values. This is browser-specific (and disableable by autocomplete="off"). What I meant with request attributes is that the exception detail can be stored in the request scope this way. If the exception handler would need to send a redirect, then the exception detail had to be stored in the session or flash scope.

Thang Pham said...

Thank you. I am not sure if omnifaces support ie6 but when I run ie6 I got blank page. This work fine with chrome, ie 7, 8 and FireFox 10. I submited a bug report for this. Ty.

pandoo said...

First of all, thank you guys for that inspiring piece of code! I tried in in my project but it seems like not every exception is handled :/ what happens if in a ajax request a exception is thrown (for some reason) within the Render Response Phase? Is it still possible in that case to change the viewRoot via context.setViewRoot(..)?

BalusC said...

@pandoo: an exception during render response cannot be handled if the response is already committed, ajax or not. However, an exception during render response in turn indicates a bug in your own code. It should at least be fixed asap so that the checks causing this exception are only performed during invoke action phase or during PreRenderViewEvent.

Jörn Ohmen said...
This comment has been removed by the author.
argiropn said...

I tried today your error handler. Th error page appears perfectly but it seems that all its <script type="text/javascript" src=".."
are ignored. Did I made something wrong ?

CG said...

Do you have an example 500.xhtml? I'm not sure how to find the exception in the session.

argirop said...

To correct my self the script code in error page is not recognized only in IE.

lleontop said...

Hi BalusC!

Your Exception Handler is great..
But when using it with PrimeFaces i face a strange problem.I posted the description here http://forum.primefaces.org/viewtopic.php?f=3&t=18937&p=67123#p67123 if you want to take a look.

Thanks!!

Lal Sah said...

Excellent post and nicely portrayed!
Great job BalusC!

Pierre said...

Hi BalusC Thanks for the Handler. Works great for us! I was wondering if you could update it to handle CDI NonexistentConversationException as well. Looks like the JSFContext context gets lost somewhere when reloading the view and I can redirect the error to a JSF page ...

Thanks!

mikeschwartz said...

Hi Bauke. I'd spent close to two hours of web searching trying to figure out why NonexistentConversationException wasn't triggering my web.xml error definition for the exception. Then I came across this blog post and you were the first to state that ViewExpiredException has this issue with AJAX. Your solution works great for NonexistentConversationException. Lots of folks on the web are running into this issue, and you had the first drop-in solution via Omnifaces. Thanks for all your contributions and time, here and on S/O, in making my JSF 2.0 experience more productive.

Jean-Charles Laurent said...

Balus

I am not sure what I am doing wrong. I have included the factory as stated and have the omnifaces-1.0.jar in my project. When on a search page, when the button search is click I launch a wait popup (using richfaces) as such:


I also have a servlet filter that checks the user role (retrieve from the session). If role is not valid it throws an exception.

Now the exception is thrown but the wait panel is still displayed and hangs. The redirect is not done.

When using a non ajax button, this work fine.

what am I missing? is there something I should add in the doFilter() method?

Thanks for you help

JC

Bauke Scholtz said...

@Jean: FullAjaxExceptionHandler works only for exceptions thrown inside JSF context and caught by FacesServlet. This Filter doesn't run inside JSF context, but right before FacesServlet is invoked, so FacesServlet has never had chance to catch it, let alone to execute the exception handler.

Your best bet is probably to perform a redirect yourself. See also this answer for hints: http://stackoverflow.com/questions/9305144/using-jsf-2-0-facelets-is-there-a-way-to-attach-a-global-listener-to-all-ajax/9311920#9311920

Jean-Charles Laurent said...

Thanks work great now.

JC

MariO said...
This comment has been removed by the author.
Bauke Scholtz said...

@MariO: this is already fixed for OmniFaces 1.4.

MariO said...

Hi BalusC! Why AbortProcessingException is not handled? there is some problem with doing it?

MariO said...

Ok! Thanks!

MariO said...

@Bauke Scholtz

sorry... i was browsing into the code, but i still see the return statement after check for AbortProcessingException. It's an uncommitted fix?

Bauke Scholtz said...

@MariO: AbortProcessingException is intented to skip remaining action listeners, not to indicate a business problem. It should therefore not be handled as an exceptional case. Previously, the FullAjaxExceptionHandler treated it as an exceptional case, but it has for OmniFaces 1.4. been fixed. It will now behave the same as in non-ajax requests. See also issue 136.

If you intend to indicate a business problem, just don't throw AbortProcessingException (or better, don't use actionListener, but action).

MariO said...

Ok! Now I understand :)! you've been very explanatory! Thanks again!

pedanticgeek said...

Hi Bauke,

Regarding the source of the FullAjaxExceptionHandler, I have a few questions:

1. Is a non-ajax request not handled on purpose so as to let the container redirect to the error page?

2. If yes, and say we need to provide some more additional info for use in test environments, how to configure it?

- Vrushank

Bauke Scholtz said...

@pedantigeek:

1. Yes.
2. You could use a filter for that. Check the FacesExceptionFilter of the OmniFaces JSF utility library for an example.

chaojun zhang said...
This comment has been removed by the author.
chaojun zhang said...

<ui:composition template="/templates/layout.xhtml">&ltui:define name="contentTitle">You got a RuntimeException! There's a BUG somewhere!</ui:define></ui:composition>

I got the above page view after normal request exception.
why?

Bauke Scholtz said...

@chaojun: you need the FacesExceptionFilter for normal request exceptions.

vineeth ng said...

have someone done a sample code,
is there a problem if using pretty faces, in the application.

vineeth ng said...

There was a session timeout filter specified in the web.xml this was handling the timeout, so it never hit the 'FacesAjaxAwareUserFilter'. It was my fault that i didnt noticed baluc told in a Note.

nena said...

BalusC - what would we do without you? You saved me hours of grief and aggravation - thanks for creating OmniFaces!!!

nena said...

I have a question - how can I extract the error message of exception caught (using OmniFaces) - within a backing bean that is referenced from my error.xhtml? I don't see the exception message in the request attributes. The exception is thrown from a filter that checks to see if a given request is valid for specific user data.

Fabian said...

I have a question regarding the ViewExpiredException. In my view there is a postAddToView event (f:event type="postAddToView" listener="#{bean.initializeView}") which depends on values from the current session. If the session timed out, and the view should be restored (e.g. by some ajax action) there are exceptions thrown (e. g. IllegalStateException), because the values are not available anymore. I would have expected that a ViewExpiredException would have been thrown there. Looking into the source code from RestoreViewPhase.class I saw that only if the viewRoot is null from restoreView-Method the ViewExpiredException is thrown. Now my question ;-) How to deal with exceptions that occur during restore view phase that are based on timed out sessions?
-
Fabian