2 Replies Latest reply on May 19, 2016 1:02 PM by cslater

    Can redirection to a context root with a trailing slash be disabled?

    cslater

      I recently set up a WildFly instance to host automated test application endpoints. 

       

      Originally I had an application with a context root of "/" and a servlet at "/demo" and everything worked as expected.

       

      jboss-web.xml

      <jboss-web>
          <context-root>/</context-root>
      </jboss-web>
      

       

      DemoServlet

      @WebServlet("/demo")
      public class DemoServlet extends HttpServlet {
      
          private static final long serialVersionUID = 7102934004098061933L;
      
          protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              response.setContentType("application/json;charset=UTF-8");
              try (PrintWriter out = response.getWriter()) {
                  out.print("{\"result\":\"success\"}");
              }
          }
          
          @Override
          protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              processRequest(request, response);
          }
      
          @Override
          protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              processRequest(request, response);
          }
      }
      
      
      
      

       

      GET Request

      curl -v http://localhost:8080/demo
      *   Trying ::1...
      * connect to ::1 port 8080 failed: Connection refused
      *   Trying 127.0.0.1...
      * Connected to localhost (127.0.0.1) port 8080 (#0)
      > GET /demo HTTP/1.1
      > Host: localhost:8080
      > User-Agent: curl/7.43.0
      > Accept: */*
      > 
      < HTTP/1.1 200 OK
      < Connection: keep-alive
      < X-Powered-By: Undertow/1
      < Server: WildFly/10
      < Content-Type: application/json;charset=UTF-8
      < Content-Length: 20
      < Date: Fri, 13 May 2016 14:22:38 GMT
      < 
      * Connection #0 to host localhost left intact
      {"result":"success"}
      

       

      POST Request

      curl -v -d "" http://localhost:8080/demo
      *   Trying ::1...
      * connect to ::1 port 8080 failed: Connection refused
      *   Trying 127.0.0.1...
      * Connected to localhost (127.0.0.1) port 8080 (#0)
      > POST /demo HTTP/1.1
      > Host: localhost:8080
      > User-Agent: curl/7.43.0
      > Accept: */*
      > Content-Length: 0
      > Content-Type: application/x-www-form-urlencoded
      > 
      < HTTP/1.1 200 OK
      < Connection: keep-alive
      < X-Powered-By: Undertow/1
      < Server: WildFly/10
      < Content-Type: application/json;charset=UTF-8
      < Content-Length: 20
      < Date: Fri, 13 May 2016 14:30:23 GMT
      < 
      * Connection #0 to host localhost left intact
      {"result":"success"}
      

       

       

      However, I then had to move my application out of "/", but needed my tests to work.  I set the context root to "/demo" and my servlet to "/" and now WildFly is returning redirects to the same URL with a slash at the end.

       

      jboss-web.xml

      <jboss-web>
          <context-root>/demo</context-root>
      </jboss-web>
      

       

      DemoServlet

      @WebServlet("/")
      public class DemoServlet extends HttpServlet {
      
          private static final long serialVersionUID = 7102934004098061933L;
      
          protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              response.setContentType("application/json;charset=UTF-8");
              try (PrintWriter out = response.getWriter()) {
                  out.print("{\"result\":\"success\"}");
              }
          }
          
          @Override
          protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              processRequest(request, response);
          }
      
          @Override
          protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              processRequest(request, response);
          }
      }
      
      
      
      

       

      GET Request

      curl -v http://localhost:8080/demo
      *   Trying ::1...
      * connect to ::1 port 8080 failed: Connection refused
      *   Trying 127.0.0.1...
      * Connected to localhost (127.0.0.1) port 8080 (#0)
      > GET /demo HTTP/1.1
      > Host: localhost:8080
      > User-Agent: curl/7.43.0
      > Accept: */*
      > 
      < HTTP/1.1 302 Found
      < Connection: keep-alive
      < X-Powered-By: Undertow/1
      < Server: WildFly/10
      < Location: http://localhost:8080/demo/
      < Content-Length: 0
      < Date: Fri, 13 May 2016 14:37:26 GMT
      < 
      * Connection #0 to host localhost left intact
      

       

      POST Request

      curl -v -d "" http://localhost:8080/demo
      *   Trying ::1...
      * connect to ::1 port 8080 failed: Connection refused
      *   Trying 127.0.0.1...
      * Connected to localhost (127.0.0.1) port 8080 (#0)
      > POST /demo HTTP/1.1
      > Host: localhost:8080
      > User-Agent: curl/7.43.0
      > Accept: */*
      > Content-Length: 0
      > Content-Type: application/x-www-form-urlencoded
      > 
      < HTTP/1.1 307 Temporary Redirect
      < Connection: keep-alive
      < X-Powered-By: Undertow/1
      < Server: WildFly/10
      < Location: http://localhost:8080/demo/
      < Content-Length: 0
      < Date: Fri, 13 May 2016 14:38:06 GMT
      * HTTP error before end of send, stop sending
      < 
      * Closing connection 0
      

       

       

      In researching this, it looks as this behavior is as designed.

       

      https://issues.jboss.org/browse/UNDERTOW-404

      https://issues.jboss.org/browse/UNDERTOW-89

       

      However, this seems incorrect to me.  The same URL in my above example has inconsistent behavior depending on location of the context root, of which clients should not be aware of.  Is there any way to disable this feature?

        • 1. Re: Can redirection to a context root with a trailing slash be disabled?
          jaikiran

          I don't remember what the Servlet specs say about the URL mapping of the servlet being / and how it behaves in the context of what you are describing. Maybe Stuart ( swd847 ) will be able to help.

          • 2. Re: Can redirection to a context root with a trailing slash be disabled?
            cslater

            I did some research and it looks like Wildfly/Undertow is following the Servlet spec with respect to static content and welcome files:

             

            From the 3.1 Servlet spec - 10.10 Welcome Files

            ■ A request URI of /foo will be redirected to a URI of /foo/.
            ■ A request URI of /foo/ will be returned as /foo/index.html.
            ■ A request URI of /catalog will be redirected to a URI of /catalog/.
            ■ A request URI of /catalog/ will be returned as /catalog/default.jsp.
            ■ A request URI of /catalog/index.html will cause a 404 not found
            ■ A request URI of /catalog/products will be redirected to a URI of
            /catalog/products/.
            ■ A request URI of /catalog/products/ will be passed to the “default” servlet, if
            any. If no “default” servlet is mapped, the request may cause a 404 not found,
            may cause a directory listing including shop.jsp and register.jsp, or may
            cause other behavior defined by the container. See Section 12.2, “Specification of
            Mappings” for the definition of “default” servlet.
            
            

             

            It is also worth noting in my above example, according to the spec, I really should have set my DemoServlet path to "" (empty string).  "/" means the default servlet.  However, changing this did not solve my problem.

             

            12.1 Use of URL Paths
            Upon receipt of a client request, the Web container determines the Web application
            to which to forward it. The Web application selected must have the longest context
            path that matches the start of the request URL. The matched part of the URL is the
            context path when mapping to servlets.
            The Web container next must locate the servlet to process the request using the path
            mapping procedure described below.
            The path used for mapping to a servlet is the request URL from the request object
            minus the context path and the path parameters. The URL path mapping rules
            below are used in order. The first successful match is used with no further matches
            attempted:
            1. The container will try to find an exact match of the path of the request to the path
            of the servlet. A successful match selects the servlet.
            2. The container will recursively try to match the longest path-prefix. This is done
            by stepping down the path tree a directory at a time, using the ’/’ character as a
            path separator. The longest match determines the servlet selected.
            3. If the last segment in the URL path contains an extension (e.g. .jsp), the servlet
            container will try to match a servlet that handles requests for the extension. An
            extension is defined as the part of the last segment after the last ’.’ character.
            4. If neither of the previous three rules result in a servlet match, the container will
            attempt to serve content appropriate for the resource requested. If a "default"
            servlet is defined for the application, it will be used. Many containers provide an
            implicit default servlet for serving content.
            The container must use case-sensitive string comparisons for matching.
            
            
            12.2 Specification of Mappings
            In the Web application deployment descriptor, the following syntax is used to define
            mappings:
            ■ A string beginning with a ‘/’ character and ending with a ‘/*’ suffix is used for
            path mapping.
            ■ A string beginning with a ‘*.’ prefix is used as an extension mapping.
            ■ The empty string ("") is a special URL pattern that exactly maps to the
            application's context root, i.e., requests of the form http://host:port/<contextroot>/.
            In this case the path info is ’/’ and the servlet path and context path is
            empty string (““).
            ■ A string containing only the ’/’ character indicates the "default" servlet of the
            application. In this case the servlet path is the request URI minus the context path
            and the path info is null.
            ■ All other strings are used for exact matches only.
            If the effective web.xml (after merging information from fragments and
            annotations) contains any url-patterns that are mapped to multiple servlets then the
            deployment must fail.
            
            
            
            

             

             

            I did find a workaround for my problem, though

             

            First I changed my servlet path to "":

             

            @WebServlet("")
            public class DemoServlet extends HttpServlet {
            
                private static final long serialVersionUID = 7102934004098061933L;
            
                protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                    response.setContentType("application/json;charset=UTF-8");
                    try (PrintWriter out = response.getWriter()) {
                        out.print("{\"result\":\"success\"}");
                    }
                }
               
                @Override
                protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                    processRequest(request, response);
                }
            
                @Override
                protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                    processRequest(request, response);
                }
            }
            
            
            
            
            

             

            Then I created a servlet at "/*" to bypass the static file / welcome content processing:

             

            @WebServlet("/*")
            public class DispatchServlet extends HttpServlet {
            
                private static final long serialVersionUID = -6906116416271053181L;
               
                private Set<String> servletMappings = null;
            
                @Override
                public void init() throws ServletException {
                    //get all servlet mappings and remove wildcard mappings
                    servletMappings = Collections.unmodifiableSet(getServletContext().getServletRegistrations().values().stream()
                            .flatMap(sr -> sr.getMappings().stream())
                            .map(m -> m.equals("") ? "/" : m)
                            .filter(m -> !m.contains("*"))
                            .collect(Collectors.toSet()));
                }
            
               
               
                @Override
                protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            
                    //This servlet will catch requests that don't match any other servlets.  This will include requests to servlets
                    //without a trailing slash.  The pathInfo will however, have a trailing slash on it.
                   
                    String pathInfo = request.getPathInfo() == null ? "" : request.getPathInfo();
                   
                    //Remove the trailing slash.
                   
                    if(!pathInfo.equals("/") && pathInfo.endsWith("/")) {
                        pathInfo = pathInfo.substring(0, pathInfo.length() - 1);
                    }
                   
                    //Check if request matched one of the non-wildcard servlets:
                    //If so, forward, else return an error.
                   
                    if(servletMappings.contains(pathInfo)){
                        request.getRequestDispatcher(pathInfo).forward(request, response);
                    } else {
                        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                        response.setContentType("text/plain;charset=UTF-8");
                        try(PrintWriter writer = response.getWriter()) {
                            writer.write("Not Found");
                        }
                    }
            
                }
            
            }
            
            
            
            
            
            
            
            

             

            While not fully production tested, this seems to do the trick and works when other servlets are added to the application outside of just the root servlet.