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]