Rabu, 09 April 2014

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 in web.xml as follows:





500
/errors/500.xhtml


javax.faces.application.ViewExpiredException
/errors/expired.xhtml



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:






org.omnifaces.exceptionhandler.FullAjaxExceptionHandlerFactory




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:





  • Date/time: #{of:formatDate(now, 'yyyy-MM-dd HH:mm:ss')}

  • HTTP user agent: #{header['user-agent']}

  • Request URI: #{requestScope['javax.servlet.error.request_uri']}

  • Status code: #{requestScope['javax.servlet.error.status_code']}

  • Exception type: #{requestScope['javax.servlet.error.exception_type']}

  • Exception message: #{requestScope['javax.servlet.error.message']}

  • Exception stack trace:
    #{of:printStackTrace(requestScope['javax.servlet.error.exception'])}





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 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("") + 6, newView.indexOf("")));
$('body').html(newView.substring(newView.indexOf("") + 6, newView.indexOf("")));
}
else {
originalPrimeFacesAjaxResponseFunction.apply(this, arguments);
}
};


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




Source:http://balusc.blogspot.com/2012/03/full-ajax-exception-handler.html

Tidak ada komentar:

Posting Komentar