Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions java/jakarta/servlet/http/HttpServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -266,6 +267,33 @@ protected void doPatch(HttpServletRequest req, HttpServletResponse resp) throws
}


/**
* 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
Expand Down Expand Up @@ -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()) {
Expand All @@ -415,10 +445,15 @@ private String getCachedAllowHeaderValue() {
allowDelete = true;
break;
}
case "doQuery": {
allowQuery = true;
break;
}
default:
// NO-OP
}


}

StringBuilder allow = new StringBuilder();
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions java/jakarta/servlet/http/LocalStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions java/org/apache/catalina/authenticator/FormAuthenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> names = saved.getHeaderNames();
while (names.hasNext()) {
String name = names.next();
Expand Down Expand Up @@ -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;
}

Expand Down
6 changes: 6 additions & 0 deletions java/org/apache/catalina/connector/CoyoteAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions java/org/apache/catalina/connector/LocalStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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}]
Expand Down
4 changes: 2 additions & 2 deletions java/org/apache/catalina/filters/ExpiresFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
11 changes: 11 additions & 0 deletions java/org/apache/tomcat/util/http/Method.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";



/**
Expand Down Expand Up @@ -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;
Expand Down
91 changes: 91 additions & 0 deletions test/jakarta/servlet/http/TestHttpServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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);
}
}
}

2 changes: 1 addition & 1 deletion test/org/apache/tomcat/util/http/TestMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions webapps/docs/changelog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@
<add>
Add the Jakarta EE 12 XML schemas. (markt)
</add>
<add>
Add support for the HTTP <code>QUERY</code> method as defined in RFC
10008. (desiderantes)
</add>
<add>
Add support for the new Servlet API method
<code>HttpServletResponse.sendEarlyHints()</code>. (markt)
Expand Down