-
Available since OmniFaces 1.0
The FullAjaxExceptionHandler
will transparently handle exceptions during ajax requests exactly the same way as exceptions during synchronous (non-ajax) requests.
By default, when an exception occurs during a Faces ajax request, the enduser would not get any form of feedback if the action was successfully performed or not. In Mojarra, only when the project stage is set to Development
, the enduser would see a bare JavaScript alert with only the exception type and message. It would make sense if exceptions during ajax requests are handled the same way as exceptions during synchronous requests, which is utilizing the standard Servlet API <error-page>
mechanisms in web.xml
.
Installation
This handler must be registered by a factory as follows in faces-config.xml
in order to get it to run:
<factory>
<exception-handler-factory>org.omnifaces.exceptionhandler.FullAjaxExceptionHandlerFactory</exception-handler-factory>
</factory>
Error pages
This exception handler will parse the web.xml
and web-fragment.xml
files to find the error page locations of the HTTP error code 500
and all declared specific exception types. Those locations need to point to Facelets files (JSP is not supported) and the URL must match the FacesServlet
mapping (just mapping it on *.xhtml
should eliminate confusion about virtual URLs). E.g.
<error-page>
<exception-type>jakarta.faces.application.ViewExpiredException</exception-type>
<location>/WEB-INF/errorpages/expired.xhtml</location>
</error-page>
The location of the HTTP error code 500
or the exception type java.lang.Throwable
is required in order to get the FullAjaxExceptionHandler
to work, because there's then at least a fall back error page when there's no match with any of the declared specific exceptions types. You can have both, but the java.lang.Throwable
one will always get precedence over all others. When you have error pages for specific exception types, then you'd better use the 500
one as fallback error page.
<error-page>
<error-code>500</error-code>
<location>/WEB-INF/errorpages/500.xhtml</location>
</error-page>
The exception detail is available in the request scope by the standard Servlet error request attributes like as in a normal synchronous error page response. You could for example show them in the error page as follows:
<ul>
<li>Date/time: #{of:formatDate(now, 'yyyy-MM-dd HH:mm:ss')}</li>
<li>User agent: #{header['user-agent']}</li>
<li>User IP: #{request.remoteAddr}</li>
<li>Request URI: #{requestScope['jakarta.servlet.error.request_uri']}</li>
<li>Ajax request: #{facesContext.partialViewContext.ajaxRequest ? 'Yes' : 'No'}</li>
<li>Status code: #{requestScope['jakarta.servlet.error.status_code']}</li>
<li>Exception type: #{requestScope['jakarta.servlet.error.exception_type']}</li>
<li>Exception message: #{requestScope['jakarta.servlet.error.message']}</li>
<li>Exception UUID: #{requestScope['org.omnifaces.exception_uuid']}</li>
<li>Stack trace:
<pre>#{of:printStackTrace(requestScope['jakarta.servlet.error.exception'])}</pre>
</li>
</ul>
Exceptions during render response can only be handled when the jakarta.faces.FACELETS_BUFFER_SIZE
is large enough so that the so far rendered response until the occurrence of the exception fits in there and can therefore safely be resetted.
Error in error page itself
When the rendering of the error page failed due to a bug in the error page itself, and the response can still be resetted, then the FullAjaxExceptionHandler
will display a hardcoded error message in "plain text" informing the developer about the double mistake.
Normal requests
Note that the FullAjaxExceptionHandler
does not deal with normal (non-ajax) requests at all. To properly handle Faces and EL exceptions on normal requests as well, you need an additional FacesExceptionFilter
. This will extract the root cause from a wrapped FacesException
and ELException
before delegating the ServletException
further to the container (the container will namely use the first root cause of ServletException
to match an error page by exception in web.xml).
Before OmniFaces 4.5, you needed to explicitly register the FacesExceptionFilter
in web.xml
. Since OmniFaces 4.5, the FullAjaxExceptionHandler
will automatically register the FacesExceptionFilter
on its default URL pattern of /*
when it is absent in web.xml
. In case you wish to map it on a different URL pattern for some reason, then you'll still need to explicitly register it in web.xml
.
Configuration
By default only FacesException
and ELException
are unwrapped. You can supply a context parameter "org.omnifaces.EXCEPTION_TYPES_TO_UNWRAP" to specify additional exception types to unwrap. The context parameter value must be a commaseparated string of fully qualified names of additional exception types. Note that this also covers subclasses of specified exception types.
<context-param>
<param-name>org.omnifaces.EXCEPTION_TYPES_TO_UNWRAP</param-name>
<param-value>jakarta.ejb.EJBException,jakarta.persistence.RollbackException</param-value>
</context-param>
This context parameter will also be read and used by FacesExceptionFilter
.
By default all exceptions are logged. You can supply a context parameter "org.omnifaces.EXCEPTION_TYPES_TO_IGNORE_IN_LOGGING" to specify exception types to ignore from logging. The context parameter value must be a commaseparated string of fully qualified names of exception types. Note that this also covers subclasses of specified exception types.
<context-param>
<param-name>org.omnifaces.EXCEPTION_TYPES_TO_IGNORE_IN_LOGGING</param-name>
<param-value>jakarta.faces.application.ViewExpiredException</param-value>
</context-param>
This context parameter will also be read and used by FacesExceptionFilter
.
This context parameter will not suppress standard Faces and/or container builtin logging. This will only suppress org.omnifaces.exceptionhandler.FullAjaxExceptionHandler
logging. So chances are that standard Faces and/or container will still log it. This may need to be configured separately.
Customizing FullAjaxExceptionHandler
If more fine grained control is desired for determining the root cause of the caught exception, or whether it should be handled, or determining the error page, or logging the exception, then the developer can opt to extend this FullAjaxExceptionHandler
and override one or more of the following protected methods:
findExceptionRootCause(FacesContext, Throwable)
shouldHandleExceptionRootCause(FacesContext, Throwable)
findErrorPageLocation(FacesContext, Throwable)
logException(FacesContext, Throwable, String, LogReason)
logException(FacesContext, Throwable, String, String, Object...)
Don't forget to create a custom ExceptionHandlerFactory
for it as well, so that it could be registered in faces-config.xml
. This does not necessarily need to extend from FullAjaxExceptionHandlerFactory
.
Demo
This exception handler is also configured on this showcase web application. The following error pages are been configured on this showcase web application:
<error-page>
<exception-type>jakarta.faces.application.ViewExpiredException</exception-type>
<location>/WEB-INF/errorpages/expired.xhtml</location>
</error-page>
<error-page>
<exception-type>java.sql.SQLException</exception-type>
<location>/WEB-INF/errorpages/database.xhtml</location>
</error-page>
<error-page>
<exception-type>java.lang.RuntimeException</exception-type>
<location>/WEB-INF/errorpages/bug.xhtml</location>
</error-page>
The buttons in the below demo will each purposefully throw an exception. You'll see that an error page template is presented regardless of if it's an ajax request or not.
Also, if you wait
30
minutes or manually delete the JSESSIONID
cookie or press the below "Invalidate session" button
and then click any of the buttons on the demo, then you'll get a view expired exception which will also end up
in a specific error page.
<p>
The buttons in the below demo will each purposefully throw an exception. You'll see that an error page template
is presented regardless of if it's an ajax request or not.
</p>
<h:form>
<h:commandButton value="throw runtime exception on ajax request" action="#{exceptionBean.throwRuntimeException}">
<f:ajax execute="@form" render="@form" />
</h:commandButton>
</h:form>
<h:form>
<h:commandButton value="throw runtime exception on normal request" action="#{exceptionBean.throwRuntimeException}" />
</h:form>
<h:form>
<h:commandButton value="throw SQL exception on ajax request" action="#{exceptionBean.throwSQLException}">
<f:ajax execute="@form" render="@form" />
</h:commandButton>
</h:form>
<h:form>
<h:commandButton value="throw SQL exception on normal request" action="#{exceptionBean.throwSQLException}" />
</h:form>
<h:form>
<h:commandButton value="throw runtime exception during render of ajax request">
<f:ajax execute="@form" render="@form" />
</h:commandButton>
<h:outputText value="#{exceptionBean.throwRuntimeException()}" rendered="#{component.namingContainer.submitted}" />
</h:form>
<h:form>
<h:commandButton value="throw runtime exception during render of normal request" />
<h:outputText value="#{exceptionBean.throwRuntimeException()}" rendered="#{component.namingContainer.submitted}" />
</h:form>
<h:form>
<h:commandButton value="cause epic fail on ajax request" action="#{exceptionBean.throwEpicFailException}">
<f:ajax execute="@form" render="@form" />
</h:commandButton>
</h:form>
<p>
Also, if you wait
#{of:formatNumber(session.maxInactiveInterval / 60, '#')}
minutes or manually delete the <code>JSESSIONID</code> cookie or press the below "Invalidate session" button
and then click any of the buttons on the demo, then you'll get a view expired exception which will also end up
in a specific error page.
</p>
<p>
<input type="button" value="Invalidate session"
onclick="$.get('#{request.contextPath}/invalidatesession?#{now.time}', alert('Session invalidated!'))" />
</p>
package org.omnifaces.showcase.exceptionhandlers;
import java.sql.SQLException;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;
@Named
@RequestScoped
public class ExceptionBean {
public void throwRuntimeException() {
throw new RuntimeException("peek-a-boo");
}
public void throwSQLException() throws SQLException {
throw new SQLException("DB fail");
}
public void throwEpicFailException() throws EpicFailException {
throw new EpicFailException();
}
}
<ui:composition template="/WEB-INF/templates/layout.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets"
xmlns:fn="jakarta.tags.functions"
xmlns:of="http://omnifaces.org/functions"
>
<ui:define name="contentTitle">Error</ui:define>
<ui:define name="content">
<ui:insert name="errorContent">
<ul>
<li>UUID: <strong>#{requestScope['org.omnifaces.exception_uuid']}</strong></li>
<li>Date/time: #{of:formatDate(now, 'yyyy-MM-dd HH:mm:ss')}</li>
<li>User agent: #{header['user-agent']}</li>
<li>User IP: #{empty header['x-forwarded-for'] ? request.remoteAddr : fn:split(header['x-forwarded-for'], ',')[0]}</li>
<li>Request URI: <a href="#{requestScope['jakarta.servlet.error.request_uri']}">#{requestScope['jakarta.servlet.error.request_uri']}</a></li>
<li>Ajax request: #{faces.ajaxRequest ? 'Yes' : 'No'}</li>
<li>Status code: #{requestScope['jakarta.servlet.error.status_code']}</li>
<li>Exception type: <code>#{requestScope['jakarta.servlet.error.exception_type']}</code></li>
<li>Exception message: <code>#{requestScope['jakarta.servlet.error.message']}</code></li>
<li>Stack trace: <pre><code>#{of:printStackTrace(requestScope['jakarta.servlet.error.exception'])}</code></pre></li>
</ul>
</ui:insert>
<h:outputScript rendered="#{faces.ajaxRequest}">scrollTo(0, 0);</h:outputScript>
</ui:define>
</ui:composition>
<ui:composition template="/WEB-INF/templates/errorpage.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="jakarta.faces.facelets"
>
<ui:define name="contentTitle">You got a RuntimeException! There's a BUG somewhere!</ui:define>
</ui:composition>
<ui:composition template="/WEB-INF/templates/errorpage.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="jakarta.faces.facelets"
>
<ui:define name="contentTitle">Oops! DB fail!</ui:define>
</ui:composition>
<ui:composition template="/WEB-INF/templates/errorpage.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="jakarta.faces.facelets"
>
<ui:define name="contentTitle">Sorry... The session has expired!</ui:define>
<ui:define name="errorContent">
<p><a href="#{requestScope['jakarta.servlet.error.request_uri']}">Back to initial page.</a></p>
</ui:define>
</ui:composition>
<ui:composition template="/WEB-INF/templates/errorpage.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="jakarta.faces.facelets"
>
<!-- This EL syntax error (a missing '}' ) should trigger a failure of error page rendering. -->
<ui:define name="errorContent">
<p><a href="#{requestScope['jakarta.servlet.error.request_uri']">Back to initial page.</a></p>
</ui:define>
</ui:composition>