This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/9.0.x by this push:
     new 14a2fcd898 Send an Allow header with the 405 response from HttpServlet
14a2fcd898 is described below

commit 14a2fcd8980699ece71d736391bd57be02058496
Author: Arturo Bernal <[email protected]>
AuthorDate: Fri Oct 23 18:11:08 2026 +0200

    Send an Allow header with the 405 response from HttpServlet
    
    Section 15.5.6 of RFC 9110 requires an origin server to generate an
    Allow header field in a 405 response that lists the methods supported by
    the target resource. The 405 response sent for a method the servlet does
    not implement did not include one. Use the value that the default
    OPTIONS handling generates.
    
    Includes back-port of OPTIONS handling from 10.1.x
---
 java/javax/servlet/http/HttpServlet.java     | 213 +++++++++++++++++----------
 test/javax/servlet/http/TestHttpServlet.java |  39 +++++
 2 files changed, 172 insertions(+), 80 deletions(-)

diff --git a/java/javax/servlet/http/HttpServlet.java 
b/java/javax/servlet/http/HttpServlet.java
index 788d3faabe..666cc7571c 100644
--- a/java/javax/servlet/http/HttpServlet.java
+++ b/java/javax/servlet/http/HttpServlet.java
@@ -86,6 +86,12 @@ public abstract class HttpServlet extends GenericServlet {
     private static final List<String> SENSITIVE_HTTP_HEADERS =
             Arrays.asList("authorization", "cookie", "x-forwarded", 
"forwarded", "proxy-authorization");
 
+    private final transient Object cachedAllowHeaderValueLock = new Object();
+
+    /**
+     * Cached value of the HTTP {@code Allow} header for this servlet.
+     */
+    private volatile String cachedAllowHeaderValue = null;
 
     /**
      * Does nothing, because this is an abstract class.
@@ -294,11 +300,96 @@ public abstract class HttpServlet extends GenericServlet {
         if (protocol.length() == 0 || protocol.endsWith("0.9") || 
protocol.endsWith("1.0")) {
             resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
         } else {
+            // RFC 9110, section 15.5.6: a 405 response must include an Allow 
header field
+            resp.setHeader("Allow", getAllowHeaderValue(req));
             resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
         }
     }
 
 
+    private String getCachedAllowHeaderValue() {
+        if (cachedAllowHeaderValue == null) {
+            synchronized (cachedAllowHeaderValueLock) {
+                if (cachedAllowHeaderValue == null) {
+
+                    Method[] methods = getAllDeclaredMethods(this.getClass());
+
+                    // RFC 7230 does not define an order for this header
+                    // This code aims to retain, broadly, the order of method
+                    // tokens returned in earlier versions of this code. If 
that
+                    // constraint is dropped then the code can be simplified
+                    // further.
+
+                    boolean allowGet = false;
+                    boolean allowHead = false;
+                    boolean allowPost = false;
+                    boolean allowPut = false;
+                    boolean allowDelete = false;
+
+                    for (Method method : methods) {
+                        switch (method.getName()) {
+                            case "doGet": {
+                                allowGet = true;
+                                allowHead = true;
+                                break;
+                            }
+                            case "doPost": {
+                                allowPost = true;
+                                break;
+                            }
+                            case "doPut": {
+                                allowPut = true;
+                                break;
+                            }
+                            case "doDelete": {
+                                allowDelete = true;
+                                break;
+                            }
+                            default:
+                                // NO-OP
+                        }
+
+                    }
+
+                    StringBuilder allow = new StringBuilder();
+
+                    if (allowGet) {
+                        allow.append(METHOD_GET);
+                        allow.append(", ");
+                    }
+
+                    if (allowHead) {
+                        allow.append(METHOD_HEAD);
+                        allow.append(", ");
+                    }
+
+                    if (allowPost) {
+                        allow.append(METHOD_POST);
+                        allow.append(", ");
+                    }
+
+                    if (allowPut) {
+                        allow.append(METHOD_PUT);
+                        allow.append(", ");
+                    }
+
+                    if (allowDelete) {
+                        allow.append(METHOD_DELETE);
+                        allow.append(", ");
+                    }
+
+                    // Options is always allowed
+                    allow.append(METHOD_OPTIONS);
+
+                    cachedAllowHeaderValue = allow.toString();
+                }
+            }
+        }
+
+        return cachedAllowHeaderValue;
+    }
+
+
     private static Method[] getAllDeclaredMethods(Class<?> c) {
 
         if (c.equals(HttpServlet.class)) {
@@ -336,95 +427,23 @@ public abstract class HttpServlet extends GenericServlet {
      * @exception ServletException if the request for the OPTIONS cannot be 
handled
      */
     protected void doOptions(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+        resp.setHeader("Allow", getAllowHeaderValue(req));
+    }
 
-        Method[] methods = getAllDeclaredMethods(this.getClass());
 
-        boolean ALLOW_GET = false;
-        boolean ALLOW_HEAD = false;
-        boolean ALLOW_POST = false;
-        boolean ALLOW_PUT = false;
-        boolean ALLOW_DELETE = false;
-        boolean ALLOW_TRACE = true;
-        boolean ALLOW_OPTIONS = true;
+    private String getAllowHeaderValue(HttpServletRequest req) {
+        String allow = getCachedAllowHeaderValue();
 
         // Tomcat specific hack to see if TRACE is allowed
-        Class<?> clazz = null;
-        try {
-            clazz = 
Class.forName("org.apache.catalina.connector.RequestFacade");
-            Method getAllowTrace = clazz.getMethod("getAllowTrace", 
(Class<?>[]) null);
-            ALLOW_TRACE = ((Boolean) getAllowTrace.invoke(req, (Object[]) 
null)).booleanValue();
-        } catch (ClassNotFoundException | NoSuchMethodException | 
SecurityException | IllegalAccessException |
-                IllegalArgumentException | InvocationTargetException e) {
-            // Ignore. Not running on Tomcat. TRACE is always allowed.
-        }
-        // End of Tomcat specific hack
-
-        for (int i = 0; i < methods.length; i++) {
-            Method m = methods[i];
-
-            if (m.getName().equals("doGet")) {
-                ALLOW_GET = true;
-                ALLOW_HEAD = true;
-            }
-            if (m.getName().equals("doPost")) {
-                ALLOW_POST = true;
-            }
-            if (m.getName().equals("doPut")) {
-                ALLOW_PUT = true;
-            }
-            if (m.getName().equals("doDelete")) {
-                ALLOW_DELETE = true;
-            }
-        }
-
-        String allow = null;
-        if (ALLOW_GET) {
-            allow = METHOD_GET;
-        }
-        if (ALLOW_HEAD) {
-            if (allow == null) {
-                allow = METHOD_HEAD;
-            } else {
-                allow += ", " + METHOD_HEAD;
-            }
-        }
-        if (ALLOW_POST) {
-            if (allow == null) {
-                allow = METHOD_POST;
-            } else {
-                allow += ", " + METHOD_POST;
-            }
-        }
-        if (ALLOW_PUT) {
-            if (allow == null) {
-                allow = METHOD_PUT;
-            } else {
-                allow += ", " + METHOD_PUT;
-            }
-        }
-        if (ALLOW_DELETE) {
-            if (allow == null) {
-                allow = METHOD_DELETE;
-            } else {
-                allow += ", " + METHOD_DELETE;
-            }
-        }
-        if (ALLOW_TRACE) {
-            if (allow == null) {
+        if (TomcatHack.getAllowTrace(req)) {
+            if (allow.length() == 0) {
                 allow = METHOD_TRACE;
             } else {
-                allow += ", " + METHOD_TRACE;
-            }
-        }
-        if (ALLOW_OPTIONS) {
-            if (allow == null) {
-                allow = METHOD_OPTIONS;
-            } else {
-                allow += ", " + METHOD_OPTIONS;
+                allow = allow + ", " + METHOD_TRACE;
             }
         }
 
-        resp.setHeader("Allow", allow);
+        return allow;
     }
 
 
@@ -624,6 +643,40 @@ public abstract class HttpServlet extends GenericServlet {
     }
 
 
+    private static class TomcatHack {
+
+        private static final Class<?> REQUEST_FACADE_CLAZZ;
+        private static final Method GET_ALLOW_TRACE;
+
+
+        static {
+            Method m1 = null;
+            Class<?> c1 = null;
+            try {
+                c1 = 
Class.forName("org.apache.catalina.connector.RequestFacade");
+                m1 = c1.getMethod("getAllowTrace", (Class<?>[]) null);
+            } catch (ReflectiveOperationException | SecurityException | 
IllegalArgumentException e) {
+                // Ignore. Not running on Tomcat. TRACE is always allowed.
+            }
+            REQUEST_FACADE_CLAZZ = c1;
+            GET_ALLOW_TRACE = m1;
+        }
+
+        public static boolean getAllowTrace(HttpServletRequest req) {
+            if (REQUEST_FACADE_CLAZZ != null && GET_ALLOW_TRACE != null) {
+                if (REQUEST_FACADE_CLAZZ.isAssignableFrom(req.getClass())) {
+                    try {
+                        return ((Boolean) GET_ALLOW_TRACE.invoke(req, 
(Object[]) null)).booleanValue();
+                    } catch (IllegalAccessException | IllegalArgumentException 
| InvocationTargetException ignore) {
+                        // Should never happen given the checks in place.
+                    }
+                }
+            }
+            return true;
+        }
+    }
+
+
     /*
      * A response wrapper for use in (dumb) "HEAD" support. This just swallows 
that body, counting the bytes in order to
      * set the content length appropriately. All other methods delegate to the 
wrapped HTTP Servlet Response object.
diff --git a/test/javax/servlet/http/TestHttpServlet.java 
b/test/javax/servlet/http/TestHttpServlet.java
index 14e5f37b7e..66b1797cc1 100644
--- a/test/javax/servlet/http/TestHttpServlet.java
+++ b/test/javax/servlet/http/TestHttpServlet.java
@@ -225,6 +225,18 @@ public class TestHttpServlet extends TomcatBaseTest {
     }
 
 
+    @Test
+    public void testMethodNotAllowed() throws Exception {
+        doTestMethodNotAllowed(new OptionsServlet(), "GET, HEAD, OPTIONS");
+    }
+
+
+    @Test
+    public void testMethodNotAllowedSub() throws Exception {
+        doTestMethodNotAllowed(new OptionsServletSub(), "GET, HEAD, POST, 
OPTIONS");
+    }
+
+
     private void doTestDoOptions(Servlet servlet, String expectedAllow) throws 
Exception {
         Tomcat tomcat = getTomcatInstance();
 
@@ -246,6 +258,33 @@ public class TestHttpServlet extends TomcatBaseTest {
     }
 
 
+    /*
+     * RFC 9110, section 15.5.6: a 405 response must contain an Allow header 
field that lists the methods supported by
+     * the target resource.
+     */
+    private void doTestMethodNotAllowed(Servlet servlet, String expectedAllow) 
throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        // No file system docBase required
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        // Map the test Servlet
+        Tomcat.addServlet(ctx, "servlet", servlet);
+        ctx.addServletMapping("/", "servlet");
+
+        tomcat.start();
+
+        Map<String,List<String>> resHeaders = new HashMap<>();
+        int rc = methodUrl("http://localhost:"; + getPort() + "/", new 
ByteChunk(), DEFAULT_CLIENT_TIMEOUT_MS, null,
+                resHeaders, Method.PUT);
+
+        Assert.assertEquals(HttpServletResponse.SC_METHOD_NOT_ALLOWED, rc);
+        List<String> allow = resHeaders.get("Allow");
+        Assert.assertNotNull(allow);
+        Assert.assertEquals(expectedAllow, allow.get(0));
+    }
+
+
     @Test
     public void testUnimplementedMethodHttp09() throws Exception {
         doTestUnimplementedMethod("0.9");


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to