views:

1099

answers:

4

We need to upgrade an elderly web application to run under GlassFish 3 instead of Tomcat in order to get EAR deployments (Glassfish was chosen as it is the reference JEE 6 implementation)

Unfortunately it very quickly turned out that the mechanism that ensures that a user is logged in does not work properly and complains that getWriter() has already been called (which is most likely correct) and I cannot figure out why.

The approach is that we have a filter on the complete set of JSP-files which checks that the user is logged in, and if not, redirects to the login page using filterChain.doFilter(servletRequest, servletResponse);. The user state (including credentials) is stored in a so called controller object in session scope which is set from the login validation java code.


Stack trace from Glassfish:

java.lang.IllegalStateException: PWC3990: getWriter() has already been called for this response
    at org.apache.catalina.connector.Response.getOutputStream(Response.java:676)
    at org.apache.catalina.connector.ResponseFacade.getOutputStream(ResponseFacade.java:205)
    at org.apache.myfaces.webapp.filter.ExtensionsFilter.doFilter(ExtensionsFilter.java:176)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:256)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:215)
    at com.XXX.LoggedInToXXXFilter.doFilter(LoggedInToXXXFilter.java:61)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:256)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:215)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:277)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:188)
....

web.xml snippet

<?xml version="1.0"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"&gt;
<description>
    XXX provides a web interface for a given user.
</description>
<display-name>
XXX
</display-name>
<context-param>
    <param-name>javax.faces.CONFIG_FILES</param-name> 
    <param-value>/WEB-INF/online-faces-config.xml</param-value>
</context-param>
<context-param>
    <param-name>org.apache.myfaces.ALLOW_JAVASCRIPT</param-name>
    <param-value>true</param-value>
</context-param> 

<listener>
    <listener-class>
        org.apache.myfaces.webapp.StartupServletContextListener
    </listener-class>
</listener>
<servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>
    javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>*.jsf</url-pattern>
</servlet-mapping>

<session-config>
    <!-- idle time in minutes before user is automatically logged out by the container -->
    <session-timeout>30</session-timeout>
</session-config>
<welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<filter>
    <filter-name>MyFacesExtensionsFilter</filter-name>
    <filter-class>
        org.apache.myfaces.webapp.filter.ExtensionsFilter
    </filter-class>
    <init-param>
        <param-name>maxFileSize</param-name>
        <param-value>1m</param-value>
        <!-- description>Set the size limit for uploaded files.
            Format: 10 - 10 bytes
            10k - 10 KB
            10m - 10 MB
            1g - 1 GB
            </description-->
    </init-param>
</filter>

<!-- extension mapping for adding <script/>, <link/>, and other resource tags to JSF-pages  -->
<filter-mapping>
    <filter-name>MyFacesExtensionsFilter</filter-name>
    <!-- servlet-name must match the name of your javax.faces.webapp.FacesServlet entry -->
    <servlet-name>Faces Servlet</servlet-name>
</filter-mapping>

<!-- extension mapping for serving page-independent resources (javascript, stylesheets, images, etc.)  -->
<filter-mapping>
    <filter-name>MyFacesExtensionsFilter</filter-name>
    <url-pattern>/faces/myFacesExtensionResource/*</url-pattern>
</filter-mapping>

<filter>
    <description>Ensure user is logged in</description>
    <filter-name>LoggedInToXXXFilter</filter-name>
    <filter-class>
        com.XXX.servlet.filters.LoggedInToXXXFilter
    </filter-class>
    <init-param>
        <param-name>signon_page</param-name>
        <param-value>/login.jsf</param-value>
    </init-param>
    <init-param>
        <param-name>autologout_page</param-name>
        <param-value>/autologout.jsp</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>LoggedInToXXXFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<!-- filter>
    <filter-name>extensionsFilter</filter-name>
    <filter-class>org.apache.myfaces.component.html.util.ExtensionsFilter</filter-class>
    <init-param>
    <param-name>uploadMaxFileSize</param-name>
    <param-value>100m</param-value>
    </init-param>
    <init-param>
    <param-name>uploadThresholdSize</param-name>
    <param-value>100k</param-value>
    </init-param>
    </filter-->
<!-- filter-mapping>
    <filter-name>extensionsFilter</filter-name>
    <url-pattern>*.jsf</url-pattern>
    </filter-mapping>
    <filter-mapping>
    <filter-name>extensionsFilter</filter-name>
    <url-pattern>/faces/*</url-pattern>
    </filter-mapping-->
<!-- error-page>
    <exception-type>java.lang.IllegalArgumentException</exception-type>
    <location>/WEB-INF/jsp/IllegalArgumentException.jsp</location>
    </error-page-->
<error-page>
    <exception-type>java.lang.RuntimeException</exception-type>
    <location>/WEB-INF/jsp/RuntimeException.jsp</location>
</error-page>
<!-- error-page>
    <exception-type>com.transaxiom.axsWHSweb.struts.action.UserIsNotLoggedInException</exception-type>
    <location>/WEB-INF/jsp/UserIsNotLoggedInException.jsp</location>
    </error-page-->
<error-page>
    <exception-type>
        com.XXX.struts.action.SecurityViolationException
    </exception-type>
    <location>/WEB-INF/jsp/SecurityViolationException.jsp</location>
</error-page>
<error-page>
    <exception-type>
        com.XXX.logic.UncheckedCommunicationException
    </exception-type>
    <location>/WEB-INF/jsp/CommunicationException.jsp</location>
</error-page>
<error-page>
    <exception-type>
        com.XXX.logic.ConnectionNotCreatedException
    </exception-type>
    <location>
        /WEB-INF/jsp/ConnectionNotCreatedException.jsp
    </location>
</error-page>
<!-- error-page>
    <exception-type>com.XXX.logic.UncheckedConnectionNotCreatedException</exception-type>
    <location>/WEB-INF/jsp/ConnectionNotCreatedException.jsp</location>
    </error-page-->
<!-- filter>
    <filter-name>MyFacesExtensionsFilter</filter-name>
    <filter-class>org.apache.myfaces.component.html.util.ExtensionsFilter</filter-class>
    <init-param>
    <param-name>maxFileSize</param-name>
    <param-value>20m</param-value>
    </init-param>
    </filter>
    <filter-mapping>
    <filter-name>MyFacesExtensionsFilter</filter-name>
    <url-pattern>*.faces</url-pattern>
    </filter-mapping-->
</web-app>

Filter code from LoggedInToXXXFilter.java:

(The stacktrace happens in the filterChain.doFilter(servletRequest, servletResponse) line.

public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
        final FilterChain filterChain) throws IOException, ServletException {
    boolean ok = false;
    if (servletRequest instanceof HttpServletRequest) {
        HttpServletRequest request = (HttpServletRequest) servletRequest;

        String servletPath = request.getServletPath();
        if ((servletPath.equals(signOnPage) == true) || servletPath.endsWith(".css") || servletPath.equals(autologoutPage)) {
            ok = true;
        } else {
            Controller controller = Controller.getControllerFromSession(request.getSession(false));
            if ((controller != null) && controller.isSignedOn()) {
                ok = true;
            }

        }
        if (ok) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            // Hop to the sign on page.
            // http://forum.java.sun.com/thread.jspa?threadID=548967&amp;messageID=2676856
            ServletContext servletContext = filterConfig.getServletContext();

            URL url = new URL(new URL(request.getRequestURL().toString()), (request.getContextPath() + signOnPage));
            ((HttpServletResponse) servletResponse).sendRedirect(url.toString());
        }
    } else {
        // Only for http requests
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

Could a possible reason be that we still bring our own JSF libraries (MyFaces 1.1.4 with Tomahawk)?

I'd appreciate any suggestions!


EDIT: Updated question with complete (but anonymized) web.xml. Note there is a lot of commented out stuff. I left it in as not to accidentially delete important information


EDIT: Experimented with the sun-web-app configuration file, and found it didn't make a difference. What is interesting is that after logging in, the login page throws the exception but I can manually navigate to the main page (also JSF) and see two more pages with functionality fine. There are three pages in addition to the login page which throws the exception.

My initial thought was that the separating feature would be the t-taglib (for Tomahawk) but that after a quick investigation does not seem to be the case as some of the working pages use Tomahawk and some doesn't.


EDIT: Comparing two jsp-pages, one which failed, another one which didn't did not reveal any obvious difference which should cause this. As it was pointed out that there has been reported this very bug with Tomahawk 1.1 and we were using 1.1.3, I have now upgraded to the latest Apache Myfaces Tomahawk 1.1.9, which appears to have resolved the issue (with no sun-web-app at all).

+1  A: 

One initial question -- if you're running this on GlassFish why does the stack trace have references to Catalina? I could be mistaken but Catalina is Tomcat's core, Grizzly is GlassFish's core.

You probably already know this but the problem is getWriter() and getOutputStream() can't be both called on one response. If you leave that sort of thing to the container, it should get it right.

So one question is, is any of your code calling getWriter()? This code isn't. I don't see anything about this that looks suspicious so I'd dig into any code upstream from this filter, if there is any?

Sean Owen
This is straight out of a Glassfish 3 fresh install - the bottom lines (which I cut away) referred to Grizzly - the catalina references puzzled me too.I'll have a look to see where, if anywhere, I actually call getWriter and getOutputStream(), but I don't think so. This is not a rocket science application. But if Catalina throws the exception, perhaps I should also see it in the latest Tomcat?
Thorbjørn Ravn Andersen
Glassfish is built on top of Tomcat with basically only the HTTP connector changed to Grizzly. Grizzly isn't a servletcontainer. It's a HTTP connector.
BalusC
Had a quick look at the Grizzly home page, and they don't explain anything at all. What exactly is a HTTP connector?
Thorbjørn Ravn Andersen
@Thorbjørn Here is the HTTP Connector documentation http://tomcat.apache.org/tomcat-6.0-doc/config/http.html. Grizzly is an NIO based HTTP connector. The best place to find information on Grizzly is Jean-Francois's blog: http://weblogs.java.net/blog/jfarcand/archive/2005/06/grizzly_an_http.html
Pascal Thivent
Indeed. Check the Tomcat link, the "Nio implementation" as described there is actually Grizzly. It's just a matter of changing `protocol` to `protocol="org.apache.coyote.http11.Http11NioProtocol"`. Also see http://balusc.blogspot.com/2009/09/webapplication-performance-tips-and.html#UseNIO for the benefits.
BalusC
+3  A: 

This can have two causes:

  1. There's another Filter in the chain before ExtensionsFilter which is (indirectly) calling getWriter().
  2. This request was been forwarded from inside a JSP file instead of a Servlet class.

In this particular case, it look like that both sendRedirect() and doFilter() has been called in the same request-response chain (because sendRedirect() may implicitly call getWriter()). When a Filter calls sendRedirect(), it should not be doing doFilter() afterwards. The posted code doesn't prove this, but maybe there are some lines been removed from it for sanitization, or there's another filter before in the chain which does exactly that.

Update: after thinking once more about this and looking in the ExtensionsFilter's source, the ExtensionsFilter actually obtains the OutputStream after filtering the request/response. So, the page, servlet or any other Javacode which has been called/executed by the URL in question has (implicitly) called the getWriter().

Update 2: Glassfish v3 ships by default with Sun Mojarra JSF 2.0 reference implementation. It might have collided somehow with the MyFaces 1.x implementation shipped in the webproject. You can instruct Glassfish v3 that you prefer to use MyFaces by setting useMyFaces or (the newer) useBundledJsf property to true in /WEB-INF/sun-web.xml. Have you used it? Give it a try.

<sun-web-app>
    <class-loader delegate="false"/>
    <property name="useBundledJsf" value="true"/>
</sun-web-app>

Also see Alternative JSF implementations on GlassFish - MyFaces and Tomahawk.

BalusC
I've added the full web.xml. I can only see MyFaces things as filters, so it might be MyFaces not being 100% correct. The version is still 1.1.3 but I was hoping I could get the application to work before migrating to JSF 2.0.
Thorbjørn Ravn Andersen
The posted method is a complete copy - only anonymized.
Thorbjørn Ravn Andersen
Did you check my update? The `ExtensionsFilter` is getting `OutputStream` AFTER processing the request and response. I.e. the `response.getOutputStream()` is been done AFTER `chain.doFilter(request, response)`. Thus, the `Writer` must have been called somewhere in the "normal" Servlet/Java/JSP code which has been executed by the particular request.
BalusC
The request was for a .jsf page, so the MyFaces filter must have kicked in first. Would it have made a difference if the "check login" filter was listed first in web.xml?
Thorbjørn Ravn Andersen
No, the stacktrace already tells that the `LoggedInToXXXFilter` is executed before `ExtensionsFilter`. I now think that there's a collision with the Glassfish builtin Sun JSF implementation, which has been exposed during processing of the actual request/response. See my second update.
BalusC
Some initial experiments offline indicated that "useBundledJsf" helped. Your hypothesis about the collision sounds likely.
Thorbjørn Ravn Andersen
+3  A: 

I don't have a full explanation (i.e. I don't know where getWriter gets called) but this might be a bug in Tomahawk 1.1.3 / MyFaces 1.1.4 as reported in Jira issues like TOMAHAWK-579 or MYFACES-1310 (with the same IllegalStateException as per Servlet specification). Note that this bug seems to be container dependent, as you are experiencing.

So, either try with more recent versions of Tomahawk / MyFaces (see the compatibility matrix) or get the patch corresponding to the fix in r442340 and apply it to the branch 1.1.3 of Tomahawk. The later option is maybe the easiest one. At least, this is what I would try.

Pascal Thivent
That was it. Upgrading the tomahawk library fixed the issue. Thanks!
Thorbjørn Ravn Andersen
+1  A: 

Try defining your filter before all other filters in web.xml.

If that doesn't work, here's how I'd proceed debugging this:

Option 1:

  1. Download Glassfish sources (or the sources of the appropriate version of Tomcat, actually)
  2. Include these sources in your sources lookup directory (for the debugger)
  3. Put a breakpoint in getWriter() of org.apache.catalina.connector.Response
  4. Check the stack to see who is calling getWriter()

Option 2:

  1. Define a new filter ontop of every other filter in web.xml
  2. Create a wrapper around the supplied HttpServletResponse, and put a breakpoint in getWriter() or new Exception().printStackTrace();

Both options have essentially the same idea. In both cases give feedback, so that we can proceed brainstorming.

Bozho