diff --git a/java/jakarta/servlet/http/HttpServlet.java b/java/jakarta/servlet/http/HttpServlet.java
index 51b636abe1fd..e4d9a6e64121 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,33 @@ protected void doPatch(HttpServletRequest req, HttpServletResponse resp) throws
}
+ /**
+ * Called by the server (via the service 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.
+ *
+ * Unlike doGet, the container does not perform automatic conditional header processing
+ * (e.g. checking If-Modified-Since using getLastModified). 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 service 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 +419,8 @@ private String getCachedAllowHeaderValue() {
boolean allowPost = false;
boolean allowPut = false;
boolean allowDelete = false;
+ boolean allowQuery = false;
+
for (Method method : methods) {
switch (method.getName()) {
@@ -415,10 +445,15 @@ private String getCachedAllowHeaderValue() {
allowDelete = true;
break;
}
+ case "doQuery": {
+ allowQuery = true;
+ break;
+ }
default:
// NO-OP
}
+
}
StringBuilder allow = new StringBuilder();
@@ -453,6 +488,12 @@ private String getCachedAllowHeaderValue() {
allow.append(", ");
}
+ if (allowQuery) {
+ allow.append(METHOD_QUERY);
+ allow.append(", ");
+ }
+
+
// Options is always allowed
allow.append(METHOD_OPTIONS);
@@ -652,6 +693,8 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
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 492548841fbf..157811fb7099 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 dde079f227da..8e49aa1a3e4c 100644
--- a/java/org/apache/catalina/authenticator/FormAuthenticator.java
+++ b/java/org/apache/catalina/authenticator/FormAuthenticator.java
@@ -596,7 +596,7 @@ protected boolean restoreRequest(Request request, Session session) throws IOExce
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 names = saved.getHeaderNames();
while (names.hasNext()) {
String name = names.next();
@@ -630,7 +630,7 @@ protected boolean restoreRequest(Request request, Session session) throws IOExce
// If no content type specified, use default for POST
String savedContentType = saved.getContentType();
- if (savedContentType == null && Method.POST.equals(method)) {
+ if (savedContentType == null && (Method.POST.equals(method) || Method.QUERY.equals(method))) {
savedContentType = Globals.CONTENT_TYPE_FORM_URL_ENCODING;
}
diff --git a/java/org/apache/catalina/connector/CoyoteAdapter.java b/java/org/apache/catalina/connector/CoyoteAdapter.java
index 92fbecffea44..cd702bbe24f7 100644
--- a/java/org/apache/catalina/connector/CoyoteAdapter.java
+++ b/java/org/apache/catalina/connector/CoyoteAdapter.java
@@ -815,6 +815,12 @@ protected boolean postParseRequest(org.apache.coyote.Request req, Request reques
return false;
}
+ // 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;
+ }
+
// Filter TRACE method
if (!connector.getAllowTrace() && Method.TRACE.equals(req.getMethod())) {
Wrapper wrapper = request.getWrapper();
diff --git a/java/org/apache/catalina/connector/LocalStrings.properties b/java/org/apache/catalina/connector/LocalStrings.properties
index 536a9a1b656f..10689d385a0b 100644
--- a/java/org/apache/catalina/connector/LocalStrings.properties
+++ b/java/org/apache/catalina/connector/LocalStrings.properties
@@ -20,6 +20,7 @@ coyoteAdapter.authorize=Authorizing user [{0}] using Tomcat''s Realm
coyoteAdapter.checkRecycled.request=Encountered a non-recycled request and recycled it forcedly.
coyoteAdapter.checkRecycled.response=Encountered a non-recycled response and recycled it forcedly.
coyoteAdapter.connect=HTTP requests using the CONNECT method are not supported
+coyoteAdapter.query.contentTypeMissing=HTTP requests using the QUERY method must have a Content-Type header
coyoteAdapter.debug=The variable [{0}] has value [{1}]
coyoteAdapter.invalidURI=Invalid URI
coyoteAdapter.invalidURIWithMessage=Invalid URI: [{0}]
diff --git a/java/org/apache/catalina/filters/ExpiresFilter.java b/java/org/apache/catalina/filters/ExpiresFilter.java
index 0bf7de33b31d..fc30b91b3700 100644
--- a/java/org/apache/catalina/filters/ExpiresFilter.java
+++ b/java/org/apache/catalina/filters/ExpiresFilter.java
@@ -1473,9 +1473,9 @@ public void init(FilterConfig filterConfig) throws ServletException {
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 648d634ee434..045cddba5a2e 100644
--- a/java/org/apache/tomcat/util/http/Method.java
+++ b/java/org/apache/tomcat/util/http/Method.java
@@ -66,6 +66,10 @@ 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 +175,13 @@ public static String bytesToString(byte[] buf, int start, int len) {
}
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 eb0caa291977..4d1e5a3dd109 100644
--- a/test/jakarta/servlet/http/TestHttpServlet.java
+++ b/test/jakarta/servlet/http/TestHttpServlet.java
@@ -225,6 +225,85 @@ public void testDoOptionsSub() throws Exception {
}
+ @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());
+ }
+
+
+
private void doTestDoOptions(Servlet servlet, String expectedAllow) throws Exception {
Tomcat tomcat = getTomcatInstance();
@@ -583,4 +662,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
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);
+ }
+ }
}
+
diff --git a/test/org/apache/tomcat/util/http/TestMethod.java b/test/org/apache/tomcat/util/http/TestMethod.java
index a5fc7b7c28b5..e72ecdf0ad58 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 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 8c0734370b33..82081525befc 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -136,6 +136,10 @@
Add the Jakarta EE 12 XML schemas. (markt)
+
+ Add support for the HTTP QUERY method as defined in RFC
+ 10008. (desiderantes)
+
Add support for the new Servlet API method
HttpServletResponse.sendEarlyHints(). (markt)