JSF Error Pages That Actually Work |
Here is an annoying problem using JSF error pages for JSF requests. Everything looks just fine, HttpServletResponse.sendError() sends the error page, but JSF continues processing and starts throwing exceptions after the response is complete. This happens even if you call FacesContext.responseComplete(), and also when the error page is sent at different stages of the JSF lifecycle.
It seems like invoking the FacesServlet for sendError() breaks the state of the original FacesContext.
When sending an error during view build I get this exception:
JSF Error Pages That Actually Workjava.lang.NullPointerException at com.sun.faces.facelets.util.Resource.getResourceUrl(Resource.java:105) at com.sun.faces.facelets.impl.DefaultResourceResolver.resolveUrl(DefaultResourceResolver.java:77) at com.sun.faces.facelets.impl.DefaultFaceletFactory.resolveURL(DefaultFaceletFactory.java:229) at com.sun.faces.facelets.impl.DefaultFacelet.getRelativePath(DefaultFacelet.java:273) at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:341) at com.sun.faces.facelets.impl.DefaultFaceletContext.includeFacelet(DefaultFaceletContext.java:199) at com.sun.faces.facelets.tag.ui.DecorateHandler.apply(DecorateHandler.java:145) at com.sun.faces.facelets.compiler.NamespaceHandler.apply(NamespaceHandler.java:93) at com.sun.faces.facelets.compiler.EncodingHandler.apply(EncodingHandler.java:86) at com.sun.faces.facelets.impl.DefaultFacelet.apply(DefaultFacelet.java:149) at com.sun.faces.application.view.FaceletViewHandlingStrategy.buildView(FaceletViewHandlingStrategy.java:838) at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:100) at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:101)
and if renderView() has already started, it gets even more obscure:
java.lang.NullPointerException at org.richfaces.skin.SkinFactoryImpl.clearSkinCaches(SkinFactoryImpl.java:94) at org.richfaces.skin.SkinFactoryPreRenderViewListener.processEvent(SkinFactoryPreRenderViewListener.java:35) at javax.faces.event.SystemEvent.processListener(SystemEvent.java:106) at com.sun.faces.application.ApplicationImpl.processListeners(ApplicationImpl.java:2169) at com.sun.faces.application.ApplicationImpl.invokeListenersFor(ApplicationImpl.java:2145) at com.sun.faces.application.ApplicationImpl.publishEvent(ApplicationImpl.java:303) at com.sun.faces.application.ApplicationImpl.publishEvent(ApplicationImpl.java:247) at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:108) at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:101) at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:139) at javax.faces.webapp.FacesServlet.service(FacesServlet.java:594)P
JSF continues to RENDER phase in an all messed up drunken way.
I couldn't believe something so basic should be so complicated.
Well it turns out there is a fairly simple solution. Calling reponse.setStatus() instead of response.sendError() does not interrupt the JSF lifecycle. This works nicely, except the original view is still rendered in spite of the error.
So all we have to do is manually render a new view (the error page) as soon as the error occurs. This doesn't break JSF state and lets the lifecycle finish without all those random exceptions.
Here's what I'm talking about.
/** * The standard request.sendError() breaks JSF state if it is called * too late in the lifecycle. This method does the same thing but * copes better with interrupting the current request. */ public void sendError(FacesContext faces, int code, String message) { try { faces.getExternalContext().setResponseStatus(code); faces.getExternalContext().getRequestMap().put ("javax.servlet.error.message", message); ViewHandler views = faces.getApplication().getViewHandler(); String template = "/error/" + code + ".xhtml"; UIViewRoot view = views.createView(faces, template); faces.setViewRoot(view); views.getViewDeclarationLanguage(faces, template). buildView(faces, view); views.renderView(faces, view); faces.responseComplete(); } catch (IOException ioe) { throw new RuntimeException(ioe); } }
This method works any time before the view has started rendering. Normally it should be triggered during the view build by an event or managed bean @PostConstruct method. In fact it also works during the render phase but you get a mixed up response (see the comments below).
Hope you find that useful.
NB: if you use this method yourself, don't forget to update the code with the correct path of your error templates.
Roger Keays is an artist, an engineer, and a student of life. He has no fixed address and has left footprints on 40-something different countries around the world. Roger is addicted to surfing. His other interests are music, psychology, languages, the proper use of semicolons, and finding good food. |