jettyembedded-jettyservlet-3.1

Embedded Jetty 9 with async Servlets throws NullPointerException at org.eclipse.jetty.server.Request.extractFormParameters(Request.java:326)


I have an application using embedded Jetty 9.2.6 with annotated async Servlets (and I use Facelets to create interface templates). There is a random exception that occurs when I access any Servlet with asyncSupported = true.

Here is one stack trace of the random exception:

09:31:42.801 [qtp1262773598-20] DEBUG c.d.a.v.c.CitiesPerStateServlet - APPTEST-BUG
java.lang.NullPointerException: null
    at org.eclipse.jetty.server.Request.extractFormParameters(Request.java:326) ~[jetty-server-9.2.6.v20141205.jar:9.2.6.v20141205]
    at org.eclipse.jetty.server.Request.extractContentParameters(Request.java:302) ~[jetty-server-9.2.6.v20141205.jar:9.2.6.v20141205]
    at org.eclipse.jetty.server.Request.extractParameters(Request.java:256) ~[jetty-server-9.2.6.v20141205.jar:9.2.6.v20141205]
    at org.eclipse.jetty.server.Request.getParameter(Request.java:827) ~[jetty-server-9.2.6.v20141205.jar:9.2.6.v20141205]
    at com.doitlabs.app99vendas.view.controller.CitiesPerStateServlet$1.run(CitiesPerStateServlet.java:55) ~[classes/:na]
    at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1173) [jetty-server-9.2.6.v20141205.jar:9.2.6.v20141205]
    at org.eclipse.jetty.server.AsyncContextState$2.run(AsyncContextState.java:168) [jetty-server-9.2.6.v20141205.jar:9.2.6.v20141205]
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:626) [jetty-util-9.2.6.v20141205.jar:9.2.6.v20141205]
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:546) [jetty-util-9.2.6.v20141205.jar:9.2.6.v20141205]
    at java.lang.Thread.run(Thread.java:745) [na:1.8.0_20]

Here is my Servlet that is referenced in the stack trace:

@WebServlet(urlPatterns = { "/controllers/cities-per-state" }, asyncSupported = true)
public class CitiesPerStateServlet extends HttpServlet {

    final Logger LOGGER = LoggerFactory.getLogger(CitiesPerStateServlet.class);

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.processRequest(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.processRequest(req, resp);
    }   

    private void processRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        AsyncContext ac = req.startAsync(req, resp);

        ac.start(new Runnable() {

            @Override
            public void run() {

                HttpServletRequest req_ = (HttpServletRequest)ac.getRequest();

                try {

                    String           stateId = req_.getParameter("state_id");
                    Collection<City> cities   = null;

                    if (stateId != null) {
                       cities = CityDAO.retrieveByState(new Long(stateId));
                    }

                    req_.setAttribute("cities", cities);
                    ac.dispatch("/pages/system-ops/campaign/cities-per-state.xhtml");

                } catch (Exception e) {

                    if (LogUtil.shouldLog(e)) {
                        LOGGER.debug(BUG, e);
                    }                    
                }                    
            }
        });    
    }
}

Here is how I start my embedded Jetty:

public class Main {

    public static void main(String[] args) throws Exception {

        String webappDirLocation = "./src/main/webapp/";

        String webPort = System.getenv("PORT");
        if (webPort == null || webPort.isEmpty()) {
            webPort = "8080";
        }

        Server server = new Server(Integer.valueOf(webPort));

        ClassList classlist = org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server);
        classlist.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration", "org.eclipse.jetty.annotations.AnnotationConfiguration");

        WebAppContext context = new WebAppContext();
        context.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*/classes/.*");
        context.setDescriptor(webappDirLocation + "/WEB-INF/web.xml");
        context.setBaseResource(new ResourceCollection(new String[] { webappDirLocation, "./target" }));
        context.setResourceAlias("/WEB-INF/classes/", "/classes/");
        context.setContextPath("/");
        context.setParentLoaderPriority(true);

        server.setHandler(context);

        server.start();
        server.join();
    }

}

The web.xml file:

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
             version="3.1">   

    <!-- Parameters ######################################################## --> 
    <context-param>
        <param-name>javax.faces.DEFAULT_SUFFIX</param-name>
        <param-value>.xhtml</param-value>
    </context-param>
    <context-param>
        <param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
        <param-value>true</param-value>
    </context-param>

    <!-- Servlet mappings ################################################## --> 
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- Listerners ######################################################## -->
    <listener>
        <listener-class>
            com.sun.faces.config.ConfigureListener
        </listener-class>
    </listener>

    <!-- Session config #################################################### -->
    <session-config>
        <session-timeout>15</session-timeout>
    </session-config>


    <!-- Welcome ########################################################### -->
    <welcome-file-list>
        <welcome-file>index.xhtml</welcome-file>
    </welcome-file-list>

</web-app>

It is a Maven project, so here is my POM.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.doitlabs.app99vendas</groupId>
    <version>1.0-SNAPSHOT</version>
    <name>app99vendas.net</name>
    <artifactId>99vendas.net</artifactId>
    <packaging>jar</packaging>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>9.2.6.v20141205</version>
        </dependency>        
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-jsp</artifactId>
            <version>9.2.6.v20141205</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-maven-plugin</artifactId>
            <version>9.2.6.v20141205</version>
        </dependency>

        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-api</artifactId>
            <version>2.2.8</version>
        </dependency>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-impl</artifactId>
            <version>2.2.8</version>
        </dependency>

        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>org.eclipse.persistence.jpa</artifactId>
            <version>2.5.2</version>
        </dependency>          
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>9.3-1102-jdbc41</version>
        </dependency>

        <dependency>
            <groupId>com.mandrillapp.wrapper.lutung</groupId>
            <artifactId>lutung</artifactId>
            <version>0.0.5</version>
        </dependency> 
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.3</version>
        </dependency>                     
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.3.5</version>
        </dependency>                  
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>   
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>

        <dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4.7</version>
        </dependency>

        <dependency> 
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.0.13</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.8</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>
</project>

Does anyone have a clue why this is happening?

If you need more information, let me know.

Thanks in advance!


Solution

  • Access to the HttpServletRequest and HttpServletResponse objects in an AsyncContext are not the same as access outside of an AsyncContext (they are not thread safe, and can even be recycled before the AsyncContext lifecycle is complete). In fact most containers recycle what they can for Garbage Collection reasons as soon as the AsyncContext is started.

    Couple this together with the HttpServletRequest.getParameter(String name) behavior (this needs to read the request body content as well to obtain any possible parameters), and you have a situation where you need to change your code.

    It would be advised to read what you can from the HttpServletRequest before you call startAsync() and pass that information into your AsyncListener.onStartAsync() or Runnable implementation.