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]