This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit a416f92476e70165c0662be7dde9e6d3d4dd9f17 Author: opencode <[email protected]> AuthorDate: Tue Oct 13 08:57:01 2026 +0200 Restore the raw HTTP method token for methods unknown to Tomcat When the internal representation of the request method migrated from MessageBytes to String, the fall-back that stored the raw method token for methods not in Method.bytesToString()'s whitelist was dropped. As a result, valid custom method tokens parsed from bytes (HTTP/1.1 and the AJP stored method) left the request method null, so HttpServletRequest.getMethod() violated the Servlet contract and HttpServlet.service() threw a NullPointerException, producing a 500 response with a server-side NPE instead of the expected 501 response. Restore the fall-back in Request.setMethod(byte[],int,int) by converting the raw token, assumed ISO-8859-1 encoded as elsewhere in the request line parsing, to a String. HTTP/2 is unaffected since it uses setMethod(String), and the null reset in recycle() that HTTP/2 uses for duplicate :method pseudo-header detection is unchanged. Add regression tests covering a custom method reaching the application unchanged and an unknown method producing a 501 response. Also, in the changelog, relocate the entry for the asynchronous timeout fix below the backport marker, as discussed out of band. --- java/org/apache/coyote/Request.java | 14 +++- .../apache/coyote/http11/TestHttp11Processor.java | 85 ++++++++++++++++++++++ webapps/docs/changelog.xml | 26 +++++-- 3 files changed, 115 insertions(+), 10 deletions(-) diff --git a/java/org/apache/coyote/Request.java b/java/org/apache/coyote/Request.java index 84e90ac7c4..88d4813fe1 100644 --- a/java/org/apache/coyote/Request.java +++ b/java/org/apache/coyote/Request.java @@ -18,6 +18,7 @@ package org.apache.coyote; import java.io.IOException; import java.io.StringReader; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.HashMap; import java.util.Map; @@ -382,14 +383,23 @@ public final class Request { } /** - * Set the HTTP method from a byte buffer. + * Set the HTTP method from a byte buffer. The bytes are assumed to be an + * ISO-8859-1 encoded representation of an HTTP method token. If the + * method is not one of the methods known to Tomcat, the raw token is + * stored as-is. * * @param buf the byte buffer * @param start the start offset * @param len the length */ public void setMethod(byte[] buf, int start, int len) { - this.method = Method.bytesToString(buf, start, len); + String method = Method.bytesToString(buf, start, len); + if (method == null) { + // Unknown method. Fall back to converting the raw method token, + // assumed to be ISO-8859-1 encoded, to a String. + method = new String(buf, start, len, StandardCharsets.ISO_8859_1); + } + this.method = method; } /** diff --git a/test/org/apache/coyote/http11/TestHttp11Processor.java b/test/org/apache/coyote/http11/TestHttp11Processor.java index 977c59ceb9..6ed4bbb838 100644 --- a/test/org/apache/coyote/http11/TestHttp11Processor.java +++ b/test/org/apache/coyote/http11/TestHttp11Processor.java @@ -1122,6 +1122,91 @@ public class TestHttp11Processor extends TomcatBaseTest { Assert.assertTrue(client.isResponse400()); } + /* + * A valid method token that is not one of the methods known to Tomcat must be passed through to the application + * unchanged rather than being reported as null. + */ + @Test + public void testUnknownMethodIsPassedToApplication() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "MethodEchoServlet", new MethodEchoServlet()); + ctx.addServletMapping("/*", "MethodEchoServlet"); + + tomcat.start(); + + // @formatter:off + String request = + "VERSION-CONTROL / HTTP/1.1" + CRLF + + "Host: any" + CRLF + + CRLF; + // @formatter:on + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + Assert.assertTrue(client.getResponseLine(), client.isResponse200()); + Assert.assertEquals("VERSION-CONTROL", client.getResponseBody()); + } + + private static class MethodEchoServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + pw.print(req.getMethod()); + } + } + + + /* + * A method that is not implemented by the servlet should produce a 501 response rather than a 500 response caused + * by a null method name. + */ + @Test + public void testUnknownMethodReturns501() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMapping("/*", "TesterServlet"); + + tomcat.start(); + + // @formatter:off + String request = + "BADMETHOD / HTTP/1.1" + CRLF + + "Host: any" + CRLF + + CRLF; + // @formatter:on + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 501 response that mentions the method. + Assert.assertTrue(client.getResponseLine(), client.isResponse501()); + Assert.assertTrue(client.getResponseBody(), + client.getResponseBody().contains("Method [BADMETHOD]")); + } + @Test public void testInconsistentHostHeader01() throws Exception { Tomcat tomcat = getTomcatInstance(); diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 0e35123b00..e23c121673 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -395,14 +395,6 @@ </subsection> <subsection name="Coyote"> <changelog> - <fix> - Keep the asynchronous timeout in place until the timeout event has - been processed, rather than clearing it when the event is dispatched. - A dispatched event may be discarded without being processed, for - example when no container thread is available to process it, which - previously left the asynchronous request running without any timeout. - (remm) - </fix> <update> Change the default value of the <code>cookiesWithoutEquals</code> attribute of the <code>Rfc6265CookieProcessor</code> from @@ -437,7 +429,25 @@ buffer used by the BIO pair. Patch submitted by Lukas Hellebrandt. (remm) </fix> + <fix> + Restore the fall-back to the raw method token when the request + method, parsed from bytes, is not one of the methods known to + Tomcat. This fall-back was lost when the internal representation of + the method changed from <code>MessageBytes</code> to + <code>String</code>, causing unknown methods to be reported as null + to the application, which typically resulted in a 500 response with + a null pointer exception instead of the expected 501 response. + (remm) + </fix> <!-- Entries for backport and removal before 12.0.0-M1 below this line --> + <fix> + Keep the asynchronous timeout in place until the timeout event has + been processed, rather than clearing it when the event is dispatched. + A dispatched event may be discarded without being processed, for + example when no container thread is available to process it, which + previously left the asynchronous request running without any timeout. + (remm) + </fix> </changelog> </subsection> <subsection name="Jasper"> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
