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]

Reply via email to