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

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


The following commit(s) were added to refs/heads/main by this push:
     new 5e01091299 Implement HTTP QUREY (RFC 10008)
5e01091299 is described below

commit 5e01091299e41bc79509b1c8d17486f85df1d872
Author: Mark Thomas <[email protected]>
AuthorDate: Thu Jun 25 20:28:12 2026 +0100

    Implement HTTP QUREY (RFC 10008)
    
    Based on pr #1026 by Mario Daniel Ruiz Saavedra.
---
 java/jakarta/servlet/http/HttpServlet.java         |  39 +++++-
 java/jakarta/servlet/http/LocalStrings.properties  |   1 +
 .../catalina/authenticator/FormAuthenticator.java  |   2 +-
 java/org/apache/catalina/connector/Connector.java  |   2 +-
 .../apache/catalina/connector/CoyoteAdapter.java   |   6 +
 .../catalina/connector/LocalStrings.properties     |   1 +
 .../org/apache/catalina/filters/ExpiresFilter.java |   4 +-
 java/org/apache/tomcat/util/http/Method.java       |  10 ++
 test/jakarta/servlet/http/TestHttpServlet.java     | 144 ++++++++++++++++++++-
 test/org/apache/tomcat/util/http/TestMethod.java   |   2 +-
 webapps/docs/changelog.xml                         |   5 +
 webapps/docs/config/ajp.xml                        |   2 +-
 webapps/docs/config/http.xml                       |   2 +-
 13 files changed, 210 insertions(+), 10 deletions(-)

diff --git a/java/jakarta/servlet/http/HttpServlet.java 
b/java/jakarta/servlet/http/HttpServlet.java
index 51b636abe1..e11006e055 100644
--- a/java/jakarta/servlet/http/HttpServlet.java
+++ b/java/jakarta/servlet/http/HttpServlet.java
@@ -80,6 +80,7 @@ public abstract class HttpServlet extends GenericServlet {
     private static final String METHOD_POST = "POST";
     private static final String METHOD_PUT = "PUT";
     private static final String METHOD_TRACE = "TRACE";
+    private static final String METHOD_QUERY = "QUERY";
 
     private static final String HEADER_IFMODSINCE = "If-Modified-Since";
     private static final String HEADER_LASTMOD = "Last-Modified";
@@ -266,6 +267,32 @@ public abstract class HttpServlet extends GenericServlet {
     }
 
 
+    /**
+     * Called by the server (via the <code>service</code> method) to allow a 
servlet to handle a QUERY request.
+     * The HTTP QUERY method is safe and idempotent and is used to query the 
target resource.
+     * <p>
+     * Unlike <code>doGet</code>, the container does not perform automatic 
conditional header processing
+     * (e.g. checking <code>If-Modified-Since</code> using 
<code>getLastModified</code>). Because a QUERY request
+     * contains a request body, reading the body to determine the modification 
time would consume the input stream,
+     * preventing the servlet from reading the query. Therefore, if 
conditional requests (RFC 10008, Section 2.6)
+     * are supported, the servlet must handle them manually inside this method.
+     *
+     * @param req  an {@link HttpServletRequest} object that contains the 
request the client has made of the servlet
+     * @param resp an {@link HttpServletResponse} object that contains the 
response the servlet sends to the client
+     *
+     * @exception IOException      if an input or output error is detected 
when the servlet handles the request
+     * @exception ServletException if the request for the QUERY could not be 
handled
+     *
+     * @see jakarta.servlet.ServletResponse#setContentType
+     *
+     * @since Servlet 6.2
+     */
+    protected void doQuery(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+        String msg = lStrings.getString("http.method_query_not_supported");
+        sendMethodNotAllowed(req, resp, msg);
+    }
+
+
     /**
      * Called by the server (via the <code>service</code> method) to allow a 
servlet to handle a POST request. The HTTP
      * POST method allows the client to send data of unlimited length to the 
Web server a single time and is useful when
@@ -391,6 +418,7 @@ public abstract class HttpServlet extends GenericServlet {
                     boolean allowPost = false;
                     boolean allowPut = false;
                     boolean allowDelete = false;
+                    boolean allowQuery = false;
 
                     for (Method method : methods) {
                         switch (method.getName()) {
@@ -415,10 +443,13 @@ public abstract class HttpServlet extends GenericServlet {
                                 allowDelete = true;
                                 break;
                             }
+                            case "doQuery": {
+                                allowQuery = true;
+                                break;
+                            }
                             default:
                                 // NO-OP
                         }
-
                     }
 
                     StringBuilder allow = new StringBuilder();
@@ -453,6 +484,11 @@ public abstract class HttpServlet extends GenericServlet {
                         allow.append(", ");
                     }
 
+                    if (allowQuery) {
+                        allow.append(METHOD_QUERY);
+                        allow.append(", ");
+                    }
+
                     // Options is always allowed
                     allow.append(METHOD_OPTIONS);
 
@@ -652,6 +688,7 @@ public abstract class HttpServlet extends GenericServlet {
             case METHOD_OPTIONS -> doOptions(req, resp);
             case METHOD_TRACE -> doTrace(req, resp);
             case METHOD_PATCH -> doPatch(req, resp);
+            case METHOD_QUERY -> doQuery(req, resp);
             default -> {
                 //
                 // Note that this means NO servlet supports whatever
diff --git a/java/jakarta/servlet/http/LocalStrings.properties 
b/java/jakarta/servlet/http/LocalStrings.properties
index 492548841f..157811fb70 100644
--- a/java/jakarta/servlet/http/LocalStrings.properties
+++ b/java/jakarta/servlet/http/LocalStrings.properties
@@ -27,6 +27,7 @@ http.method_delete_not_supported=HTTP method DELETE is not 
supported by this URL
 http.method_get_not_supported=HTTP method GET is not supported by this URL
 http.method_not_implemented=Method [{0}] is not implemented by this Servlet 
for this URI
 http.method_patch_not_supported=HTTP method PATCH is not supported by this URL
+http.method_query_not_supported=HTTP method QUERY is not supported by this URL
 http.method_post_not_supported=HTTP method POST is not supported by this URL
 http.method_put_not_supported=HTTP method PUT is not supported by this URL
 http.non_http=Non HTTP request or response
diff --git a/java/org/apache/catalina/authenticator/FormAuthenticator.java 
b/java/org/apache/catalina/authenticator/FormAuthenticator.java
index dde079f227..3157a70d7b 100644
--- a/java/org/apache/catalina/authenticator/FormAuthenticator.java
+++ b/java/org/apache/catalina/authenticator/FormAuthenticator.java
@@ -596,7 +596,7 @@ public class FormAuthenticator extends AuthenticatorBase {
         String method = saved.getMethod();
         MimeHeaders rmh = request.getCoyoteRequest().getMimeHeaders();
         rmh.recycle();
-        boolean cacheable = Method.GET.equals(method) || 
Method.HEAD.equals(method);
+        boolean cacheable = Method.GET.equals(method) || 
Method.HEAD.equals(method) || Method.QUERY.equals(method);
         Iterator<String> names = saved.getHeaderNames();
         while (names.hasNext()) {
             String name = names.next();
diff --git a/java/org/apache/catalina/connector/Connector.java 
b/java/org/apache/catalina/connector/Connector.java
index e3f89a0eb4..c39833ff65 100644
--- a/java/org/apache/catalina/connector/Connector.java
+++ b/java/org/apache/catalina/connector/Connector.java
@@ -269,7 +269,7 @@ public class Connector extends LifecycleMBeanBase {
      * Comma-separated list of HTTP methods that will be parsed according to 
POST-style rules for
      * application/x-www-form-urlencoded request bodies.
      */
-    protected String parseBodyMethods = Method.POST;
+    protected String parseBodyMethods = StringUtils.join(Method.POST, 
Method.QUERY);
 
     /**
      * A Set of methods determined by {@link #parseBodyMethods}.
diff --git a/java/org/apache/catalina/connector/CoyoteAdapter.java 
b/java/org/apache/catalina/connector/CoyoteAdapter.java
index 92fbecffea..05dcad99cc 100644
--- a/java/org/apache/catalina/connector/CoyoteAdapter.java
+++ b/java/org/apache/catalina/connector/CoyoteAdapter.java
@@ -794,6 +794,12 @@ public class CoyoteAdapter implements Adapter {
             }
         }
 
+        // Filter QUERY method without Content-Type
+        if (Method.QUERY.equals(req.getMethod()) && req.getContentType() == 
null) {
+            response.sendError(HttpServletResponse.SC_BAD_REQUEST, 
sm.getString("coyoteAdapter.query.contentTypeMissing"));
+            return true;
+        }
+
         // Possible redirect
         MessageBytes redirectPathMB = request.getMappingData().redirectPath;
         if (!redirectPathMB.isNull()) {
diff --git a/java/org/apache/catalina/connector/LocalStrings.properties 
b/java/org/apache/catalina/connector/LocalStrings.properties
index 536a9a1b65..aff818f742 100644
--- a/java/org/apache/catalina/connector/LocalStrings.properties
+++ b/java/org/apache/catalina/connector/LocalStrings.properties
@@ -24,6 +24,7 @@ coyoteAdapter.debug=The variable [{0}] has value [{1}]
 coyoteAdapter.invalidURI=Invalid URI
 coyoteAdapter.invalidURIWithMessage=Invalid URI: [{0}]
 coyoteAdapter.nullRequest=An asynchronous dispatch may only happen on an 
existing request
+coyoteAdapter.query.contentTypeMissing=HTTP requests using the QUERY method 
must have a Content-Type header
 coyoteAdapter.trace=TRACE method is not allowed
 
 coyoteConnector.invalidEncoding=The encoding [{0}] is not recognised by the 
JRE. The Connector will continue to use [{1}]
diff --git a/java/org/apache/catalina/filters/ExpiresFilter.java 
b/java/org/apache/catalina/filters/ExpiresFilter.java
index 0bf7de33b3..fc30b91b37 100644
--- a/java/org/apache/catalina/filters/ExpiresFilter.java
+++ b/java/org/apache/catalina/filters/ExpiresFilter.java
@@ -1473,9 +1473,9 @@ public class ExpiresFilter extends FilterBase {
     protected boolean 
isEligibleToExpirationHeaderGeneration(HttpServletRequest request,
             XHttpServletResponse response) {
 
-        // Don't add cache headers unless the request is a GET or a HEAD 
request
+        // Don't add cache headers unless the request is a GET, HEAD, or QUERY 
request
         String method = request.getMethod();
-        if (!Method.GET.equals(method) && !Method.HEAD.equals(method)) {
+        if (!Method.GET.equals(method) && !Method.HEAD.equals(method) && 
!Method.QUERY.equals(method)) {
             if (log.isDebugEnabled()) {
                 log.debug(sm.getString("expiresFilter.invalidMethod", 
request.getRequestURI(), method));
             }
diff --git a/java/org/apache/tomcat/util/http/Method.java 
b/java/org/apache/tomcat/util/http/Method.java
index 648d634ee4..7efb9fac31 100644
--- a/java/org/apache/tomcat/util/http/Method.java
+++ b/java/org/apache/tomcat/util/http/Method.java
@@ -66,6 +66,9 @@ public class Method {
     // Other methods recognised by Tomcat
     /** CONNECT method. */
     public static final String CONNECT = "CONNECT";
+    // RFC 10008 HTTP QUERY method
+    /** QUERY method. */
+    public static final String QUERY = "QUERY";
 
 
     /**
@@ -171,6 +174,13 @@ public class Method {
                 }
                 break;
             }
+            case 'Q': {
+                if (len == 5 && buf[start + 1] == 'U' && buf[start + 2] == 'E' 
&& buf[start + 3] == 'R' &&
+                        buf[start + 4] == 'Y') {
+                    return QUERY;
+                }
+                break;
+            }
         }
 
         return null;
diff --git a/test/jakarta/servlet/http/TestHttpServlet.java 
b/test/jakarta/servlet/http/TestHttpServlet.java
index eb0caa2919..b92febb99d 100644
--- a/test/jakarta/servlet/http/TestHttpServlet.java
+++ b/test/jakarta/servlet/http/TestHttpServlet.java
@@ -225,6 +225,123 @@ public class TestHttpServlet extends TomcatBaseTest {
     }
 
 
+    @Test
+    public void testDoOptionsQuery() throws Exception {
+        doTestDoOptions(new OptionsServletQuery(), "GET, HEAD, QUERY, 
OPTIONS");
+    }
+
+
+    @Test
+    public void testQueryMethod() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        // No file system docBase required
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        // Map the test Servlet
+        OptionsServletQuery servlet = new OptionsServletQuery();
+        Tomcat.addServlet(ctx, "servlet", servlet);
+        ctx.addServletMappingDecoded("/", "servlet");
+
+        tomcat.start();
+
+        SimpleHttpClient client = new SimpleHttpClient() {
+            @Override
+            public boolean isResponseBodyOK() {
+                return true;
+            }
+        };
+        client.setPort(getPort());
+        client.setRequest(new String[] {
+                "QUERY / HTTP/1.1" + CRLF +
+                    "Host: localhost:" + getPort() + CRLF +
+                    "Content-Type: text/plain" + CRLF +
+                    "Connection: close" + CRLF +
+                    CRLF
+        });
+        client.connect();
+        client.sendRequest();
+        client.readResponse(true);
+
+        Assert.assertTrue(client.isResponse200());
+        Assert.assertEquals("OK", client.getResponseBody());
+    }
+
+
+    @Test
+    public void testQueryMethodWithoutContentType() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        // No file system docBase required
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        // Map the test Servlet
+        OptionsServletQuery servlet = new OptionsServletQuery();
+        Tomcat.addServlet(ctx, "servlet", servlet);
+        ctx.addServletMappingDecoded("/", "servlet");
+
+        tomcat.start();
+
+        SimpleHttpClient client = new SimpleHttpClient() {
+            @Override
+            public boolean isResponseBodyOK() {
+                return true;
+            }
+        };
+        client.setPort(getPort());
+        client.setRequest(new String[] {
+                "QUERY / HTTP/1.1" + CRLF +
+                    "Host: localhost:" + getPort() + CRLF +
+                    "Connection: close" + CRLF +
+                    CRLF
+        });
+        client.connect();
+        client.sendRequest();
+        client.readResponse(true);
+
+        Assert.assertTrue(client.isResponse400());
+    }
+
+
+    @Test
+    public void testQueryParameters() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        // No file system docBase required
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        // Map the test Servlet
+        ParamsServlet servlet = new ParamsServlet();
+        Tomcat.addServlet(ctx, "servlet", servlet);
+        ctx.addServletMappingDecoded("/", "servlet");
+
+        tomcat.start();
+
+        SimpleHttpClient client = new SimpleHttpClient() {
+            @Override
+            public boolean isResponseBodyOK() {
+                return true;
+            }
+        };
+        client.setPort(getPort());
+        client.setRequest(new String[] {
+                "QUERY /?baz=123 HTTP/1.1" + CRLF +
+                    "Host: localhost:" + getPort() + CRLF +
+                    "Content-Type: application/x-www-form-urlencoded" + CRLF +
+                    "Content-Length: 15" + CRLF +
+                    "Connection: close" + CRLF +
+                    CRLF +
+                    "foo=abc&bar=xyz"
+        });
+        client.connect();
+        client.sendRequest();
+        client.readResponse(true);
+
+        Assert.assertTrue(client.isResponse200());
+        Assert.assertEquals("abc,xyz,123", client.getResponseBody());
+    }
+
+
     private void doTestDoOptions(Servlet servlet, String expectedAllow) throws 
Exception {
         Tomcat tomcat = getTomcatInstance();
 
@@ -262,8 +379,6 @@ public class TestHttpServlet extends TomcatBaseTest {
      * See org.apache.coyote.http2.TestHttpServlet for the HTTP/2 version of 
this test. It was placed in that package
      * because it needed access to package private classes.
      */
-
-
     private void doTestUnimplementedMethod(String httpVersion) {
         StringBuilder request = new StringBuilder("PUT /test");
         boolean isHttp09 = "0.9".equals(httpVersion);
@@ -583,4 +698,29 @@ public class TestHttpServlet extends TomcatBaseTest {
             doGet(req, resp);
         }
     }
+
+
+    private static class OptionsServletQuery extends OptionsServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void doQuery(HttpServletRequest req, HttpServletResponse 
resp) throws ServletException, IOException {
+            doGet(req, resp);
+        }
+    }
+
+
+    private static class ParamsServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void doQuery(HttpServletRequest req, HttpServletResponse 
resp) throws ServletException, IOException {
+            resp.setContentType("text/plain");
+            resp.setCharacterEncoding("UTF-8");
+            PrintWriter pw = resp.getWriter();
+            pw.print(req.getParameter("foo") + "," + req.getParameter("bar") + 
"," + req.getParameter("baz"));
+        }
+    }
 }
diff --git a/test/org/apache/tomcat/util/http/TestMethod.java 
b/test/org/apache/tomcat/util/http/TestMethod.java
index a5fc7b7c28..e72ecdf0ad 100644
--- a/test/org/apache/tomcat/util/http/TestMethod.java
+++ b/test/org/apache/tomcat/util/http/TestMethod.java
@@ -32,7 +32,7 @@ public class TestMethod {
     public void testHttpMethodParsing() {
         List<String> methods = Arrays.asList(Method.GET, Method.POST, 
Method.PUT, Method.PATCH, Method.HEAD,
                 Method.OPTIONS, Method.DELETE, Method.TRACE, Method.PROPPATCH, 
Method.PROPFIND, Method.MKCOL,
-                Method.COPY, Method.MOVE, Method.LOCK, Method.UNLOCK, 
Method.CONNECT);
+                Method.COPY, Method.MOVE, Method.LOCK, Method.UNLOCK, 
Method.CONNECT, Method.QUERY);
 
         for (String method : methods) {
             byte[] bytes = method.getBytes(StandardCharsets.ISO_8859_1);
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 01a487fcd3..a726b3ed70 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -225,6 +225,11 @@
         Reject BASIC authorization with empty user names as required by RFC
         7613. (markt)
       </fix>
+      <add>
+        Add support for the HTTP <code>QUERY</code> method as defined in RFC
+        10008. Based on pull request <pr>1026</pr> by Mario Daniel Ruiz
+        Saavedra. (markt)
+      </add>
       <!-- Entries for backport and removal before 12.0.0-M1 below this line 
-->
       <fix>
         Avoid a race condition with concurrent lookups for a singleton JNDI
diff --git a/webapps/docs/config/ajp.xml b/webapps/docs/config/ajp.xml
index f1c33155a4..e4df3fd4ce 100644
--- a/webapps/docs/config/ajp.xml
+++ b/webapps/docs/config/ajp.xml
@@ -247,7 +247,7 @@
       specification.
       The HTTP method TRACE is specifically forbidden here in accordance
       with the HTTP specification.
-      The default is <code>POST</code></p>
+      The default is <code>POST,QUERY</code></p>
     </attribute>
 
     <attribute name="port" required="true">
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index 49dd080e08..33f357f71c 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -245,7 +245,7 @@
       specification.
       The HTTP method TRACE is specifically forbidden here in accordance
       with the HTTP specification.
-      The default is <code>POST</code></p>
+      The default is <code>POST,QUERY</code></p>
     </attribute>
 
     <attribute name="port" required="true">


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

Reply via email to