Skip to content

Embedded Jetty ServletContextHandler unexpectedly returns 405 for POST requests #15221

Description

@brettkail-wk

Jetty version(s)
12.1.9

Jetty Environment
ee

HTTP version
N/A

Java version/vendor (use: java -version)
N/A

OS type/version
N/A

Description
When using embedded Jetty with a ServletContextHandler (not a WAR), requests without a matching servlet are handled by ServletHandler.Default404Servlet (not DefaultServlet). For this configuration, it seems like Default404Servlet should return 404 for all methods, not just GET.

I've looked at related issues #11889 (and #10231), but those seem to be discussing DefaultServlet, where 405 is reasonable since the servlet is matching all possible resources, and POST isn't supported in that context.

I found ServletHandler.setEnsureDefaultServlet(false), and that does produce 404 for POST, but it also prevents filters from running. It seems like the intent of Default404Servlet (commit c21ce11, bug 417023) was to enable filters to run but otherwise to not change the 404 handling. It seems like the intent of setEnsureDefaultServlet (commit 8855b79, bug 433431) was to allow fallback handling, not as a workaround to avoid the 405 handling.

How to reproduce?
Here is a crude test program demonstrating the scenarios. It starts a server and then executes requests for GET+POST on an unmatched context, a matched context with Default404Servlet, and a matched context without Default404Servlet.

public static void main(String[] args) throws Exception {
  final int port = 8000;
  Server server = new Server(port);
  HttpFilter filter = new HttpFilter() {
    @Override
    protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
      System.out.println("- " + req.getRequestURL());
      chain.doFilter(req, res);;
    }
  };
  ServletContextHandler ch1 = new ServletContextHandler("/test-context1");
  ch1.addFilter(filter, "/*", null);
  ServletContextHandler ch2 = new ServletContextHandler("/test-context2");
  ch2.addFilter(filter, "/*", null);
  ch2.getServletHandler().setEnsureDefaultServlet(false);
  server.setHandler(new ContextHandlerCollection(ch1, ch2));
  server.start();

  try (var closeable = (AutoCloseable) server::stop) {
    for (String path : new String[] {
        "/doesnotexist",
        "/test-context1/doesnotexist",
        "/test-context2/doesnotexist"
    }) {
      for (String method : new String[] { "GET", "POST" }) {
        URL url = URI.create("http://localhost:" + port + path).toURL();
        HttpURLConnection uc = (HttpURLConnection) url.openConnection();
        uc.setRequestMethod(method);
        int status = uc.getResponseCode();
        String s;
        try {
          s = new String(uc.getInputStream().readAllBytes());
        } catch (IOException e) {
          s = e.toString();
        }
        System.out.println(method + " " + url + "\n\t" + status + ": " + s);
        uc.disconnect();
      }
    }
  }
}

I expected 404 for all combinations, but POST on the matched context with Default404Servlet produces 405. It seems like changing Default404Servlet to override service rather than doGet would give the desired result.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugFor general bugs on Jetty side

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions