diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/client/RestTestClientTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/client/RestTestClientTests.java
index ef10b9822114..5ef2b8f2f26f 100644
--- a/spring-test/src/test/java/org/springframework/test/web/servlet/client/RestTestClientTests.java
+++ b/spring-test/src/test/java/org/springframework/test/web/servlet/client/RestTestClientTests.java
@@ -118,7 +118,7 @@ void options() {
RestTestClientTests.this.client.options().uri("/test")
.exchange()
.expectStatus().isOk()
- .expectHeader().valueEquals("Allow", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS")
+ .expectHeader().valueEquals("Allow", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,QUERY")
.expectBody().isEmpty();
}
diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
index 12216b83d5a1..bcedd663b453 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
@@ -124,6 +124,12 @@ public class HttpHeaders implements Serializable {
* @see Section 3.1 of RFC 5789
*/
public static final String ACCEPT_PATCH = "Accept-Patch";
+ /**
+ * The HTTP {@code Accept-Query} header field name.
+ * @since 7.1
+ * @see Section 3 of RFC 10008
+ */
+ public static final String ACCEPT_QUERY = "Accept-Query";
/**
* The HTTP {@code Accept-Ranges} header field name.
* @see Section 5.3.5 of RFC 7233
@@ -648,6 +654,27 @@ public List getAcceptPatch() {
return MediaType.parseMediaTypes(get(ACCEPT_PATCH));
}
+ /**
+ * Set the list of acceptable {@linkplain MediaType media types} for
+ * {@code QUERY} methods, as specified by the {@code Accept-Query} header.
+ * @since 7.1
+ */
+ public void setAcceptQuery(List mediaTypes) {
+ set(ACCEPT_QUERY, MediaType.toString(mediaTypes));
+ }
+
+ /**
+ * Return the list of acceptable {@linkplain MediaType media types} for
+ * {@code QUERY} methods, as specified by the {@code Accept-Query} header.
+ *
Returns an empty list when the acceptable media types are unspecified.
+ * @since 7.1
+ */
+ public List getAcceptQuery() {
+ return MediaType.parseMediaTypes(get(ACCEPT_QUERY));
+ }
+
+
+
/**
* Set the (new) value of the {@code Access-Control-Allow-Credentials} response header.
*/
diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java
index 422abf282031..4c8c02791ad3 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java
@@ -39,25 +39,25 @@ public final class HttpMethod implements Comparable, Serializable {
/**
* The HTTP method {@code GET}.
- * @see HTTP 1.1, section 9.3
+ * @see HTTP Semantics, section 9.3.1
*/
public static final HttpMethod GET = new HttpMethod("GET");
/**
* The HTTP method {@code HEAD}.
- * @see HTTP 1.1, section 9.4
+ * @see HTTP Semantics, section 9.3.2
*/
public static final HttpMethod HEAD = new HttpMethod("HEAD");
/**
* The HTTP method {@code POST}.
- * @see HTTP 1.1, section 9.5
+ * @see HTTP Semantics, section 9.3.3
*/
public static final HttpMethod POST = new HttpMethod("POST");
/**
* The HTTP method {@code PUT}.
- * @see HTTP 1.1, section 9.6
+ * @see HTTP Semantics, section 9.3.4
*/
public static final HttpMethod PUT = new HttpMethod("PUT");
@@ -69,23 +69,30 @@ public final class HttpMethod implements Comparable, Serializable {
/**
* The HTTP method {@code DELETE}.
- * @see HTTP 1.1, section 9.7
+ * @see HTTP Semantics, section 9.3.5
*/
public static final HttpMethod DELETE = new HttpMethod("DELETE");
/**
* The HTTP method {@code OPTIONS}.
- * @see HTTP 1.1, section 9.2
+ * @see HTTP Semantics, section 9.3.7
*/
public static final HttpMethod OPTIONS = new HttpMethod("OPTIONS");
/**
* The HTTP method {@code TRACE}.
- * @see HTTP 1.1, section 9.8
+ * @see HTTP Semantics, section 9.3.8
*/
public static final HttpMethod TRACE = new HttpMethod("TRACE");
- private static final HttpMethod[] values = new HttpMethod[] { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE };
+ /**
+ * The HTTP method {@code QUERY}.
+ * @since 7.1
+ * @see The HTTP QUERY Method, section 2
+ */
+ public static final HttpMethod QUERY = new HttpMethod("QUERY");
+
+ private static final HttpMethod[] values = new HttpMethod[] { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, QUERY };
private final String name;
@@ -99,7 +106,7 @@ private HttpMethod(String name) {
* Returns an array containing the standard HTTP methods. Specifically,
* this method returns an array containing {@link #GET}, {@link #HEAD},
* {@link #POST}, {@link #PUT}, {@link #PATCH}, {@link #DELETE},
- * {@link #OPTIONS}, and {@link #TRACE}.
+ * {@link #OPTIONS}, {@link #TRACE}, and {@link #QUERY}.
*
*
Note that the returned value does not include any HTTP methods defined
* in WebDav.
@@ -137,6 +144,7 @@ public static HttpMethod valueOf(String method) {
case "DELETE" -> DELETE;
case "OPTIONS" -> OPTIONS;
case "TRACE" -> TRACE;
+ case "QUERY" -> QUERY;
default -> new HttpMethod(method);
};
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java
index cc7ebcc776c9..826a016f7de4 100644
--- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java
+++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java
@@ -32,6 +32,7 @@
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpTrace;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.config.Configurable;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClients;
@@ -300,6 +301,9 @@ else if (HttpMethod.OPTIONS.equals(httpMethod)) {
else if (HttpMethod.TRACE.equals(httpMethod)) {
return new HttpTrace(uri);
}
+ else if (HttpMethod.QUERY.equals(httpMethod)) {
+ return new HttpUriRequestBase(HttpMethod.QUERY.name(), uri);
+ }
throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod);
}
diff --git a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java
index 89428c5221fb..52991fce189e 100644
--- a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java
+++ b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java
@@ -132,6 +132,9 @@ public HttpHeaders getHeaders() {
if (HttpMethod.PATCH.equals(this.httpMethod)) {
headers.setAcceptPatch(getSupportedMediaTypes());
}
+ else if (HttpMethod.QUERY.equals(this.httpMethod)) {
+ headers.setAcceptQuery(getSupportedMediaTypes());
+ }
return headers;
}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java
index c4d353612080..5e38f7b6b679 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java
@@ -26,7 +26,7 @@
* {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation.
*
*
Note that, by default, {@link org.springframework.web.servlet.DispatcherServlet}
- * supports GET, HEAD, POST, PUT, PATCH, and DELETE only. DispatcherServlet will
+ * supports GET, QUERY, HEAD, POST, PUT, PATCH, and DELETE only. DispatcherServlet will
* process TRACE and OPTIONS with the default HttpServlet behavior unless explicitly
* told to dispatch those request types as well: Check out the "dispatchOptionsRequest"
* and "dispatchTraceRequest" properties, switching them to "true" if necessary.
@@ -39,7 +39,7 @@
*/
public enum RequestMethod {
- GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
+ GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, QUERY;
/**
@@ -60,6 +60,7 @@ public enum RequestMethod {
case "DELETE" -> DELETE;
case "OPTIONS" -> OPTIONS;
case "TRACE" -> TRACE;
+ case "QUERY" -> QUERY;
default -> null;
};
}
@@ -92,6 +93,7 @@ public HttpMethod asHttpMethod() {
case DELETE -> HttpMethod.DELETE;
case OPTIONS -> HttpMethod.OPTIONS;
case TRACE -> HttpMethod.TRACE;
+ case QUERY -> HttpMethod.QUERY;
};
}
diff --git a/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java b/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java
index a9a1d2024278..09a180805b14 100644
--- a/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java
+++ b/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java
@@ -47,7 +47,7 @@
import org.springframework.util.StringUtils;
/**
- * {@code Filter} that parses form data for HTTP PUT, PATCH, and DELETE requests
+ * {@code Filter} that parses form data for HTTP PUT, PATCH, DELETE, and QUERY requests
* and exposes it as Servlet request parameters. By default, the Servlet spec
* only requires this for HTTP POST.
*
@@ -56,7 +56,7 @@
*/
public class FormContentFilter extends OncePerRequestFilter {
- private static final List HTTP_METHODS = Arrays.asList("PUT", "PATCH", "DELETE");
+ private static final List HTTP_METHODS = Arrays.asList("PUT", "PATCH", "DELETE", "QUERY");
private FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
diff --git a/spring-web/src/main/java/org/springframework/web/filter/HiddenHttpMethodFilter.java b/spring-web/src/main/java/org/springframework/web/filter/HiddenHttpMethodFilter.java
index 68c703f05bb4..3654673b0887 100644
--- a/spring-web/src/main/java/org/springframework/web/filter/HiddenHttpMethodFilter.java
+++ b/spring-web/src/main/java/org/springframework/web/filter/HiddenHttpMethodFilter.java
@@ -38,7 +38,7 @@
* is to use a normal POST with an additional hidden form field ({@code _method})
* to pass the "real" HTTP method along. This filter reads that parameter and changes
* the {@link HttpServletRequestWrapper#getMethod()} return value accordingly.
- * Only {@code "PUT"}, {@code "DELETE"} and {@code "PATCH"} HTTP methods are allowed.
+ * Only {@code "PUT"}, {@code "DELETE"}, {@code "PATCH"}, and {@code "QUERY"} HTTP methods are allowed.
*
*
The name of the request parameter defaults to {@code _method}, but can be
* adapted via the {@link #setMethodParam(String) methodParam} property.
@@ -55,7 +55,7 @@
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List ALLOWED_METHODS =
- List.of(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name());
+ List.of(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name(), HttpMethod.QUERY.name());
/** Default method parameter: {@code _method}. */
public static final String DEFAULT_METHOD_PARAM = "_method";
diff --git a/spring-web/src/main/java/org/springframework/web/filter/reactive/HiddenHttpMethodFilter.java b/spring-web/src/main/java/org/springframework/web/filter/reactive/HiddenHttpMethodFilter.java
index aef52469b949..7fc0b2167fc5 100644
--- a/spring-web/src/main/java/org/springframework/web/filter/reactive/HiddenHttpMethodFilter.java
+++ b/spring-web/src/main/java/org/springframework/web/filter/reactive/HiddenHttpMethodFilter.java
@@ -47,7 +47,7 @@
public class HiddenHttpMethodFilter implements WebFilter {
private static final List ALLOWED_METHODS =
- List.of(HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.PATCH);
+ List.of(HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.PATCH, HttpMethod.QUERY);
/** Default name of the form parameter with the HTTP method to use. */
public static final String DEFAULT_METHOD_PARAMETER_NAME = "_method";
diff --git a/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java
index dc5dbfad2b85..6b5124e76454 100644
--- a/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java
+++ b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java
@@ -161,6 +161,9 @@ public HttpHeaders getHeaders() {
if (this.method == HttpMethod.PATCH) {
headers.setAcceptPatch(this.supportedMediaTypes);
}
+ else if (this.method == HttpMethod.QUERY) {
+ headers.setAcceptQuery(this.supportedMediaTypes);
+ }
return headers;
}
diff --git a/spring-web/src/test/java/org/springframework/http/HttpMethodTests.java b/spring-web/src/test/java/org/springframework/http/HttpMethodTests.java
index ac3a3f0ae4e6..a24aa6079028 100644
--- a/spring-web/src/test/java/org/springframework/http/HttpMethodTests.java
+++ b/spring-web/src/test/java/org/springframework/http/HttpMethodTests.java
@@ -44,12 +44,12 @@ void comparison() {
void values() {
HttpMethod[] values = HttpMethod.values();
assertThat(values).containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST, HttpMethod.PUT,
- HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE);
+ HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE, HttpMethod.QUERY);
// check defensive copy
values[0] = HttpMethod.POST;
assertThat(HttpMethod.values()).containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST, HttpMethod.PUT,
- HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE);
+ HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE, HttpMethod.QUERY);
}
@Test
diff --git a/spring-web/src/test/java/org/springframework/web/bind/annotation/RequestMethodTests.java b/spring-web/src/test/java/org/springframework/web/bind/annotation/RequestMethodTests.java
index a16fb2b84d70..a7f7fb31fc1d 100644
--- a/spring-web/src/test/java/org/springframework/web/bind/annotation/RequestMethodTests.java
+++ b/spring-web/src/test/java/org/springframework/web/bind/annotation/RequestMethodTests.java
@@ -29,7 +29,7 @@ class RequestMethodTests {
@Test
void resolveString() {
- String[] methods = new String[]{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE"};
+ String[] methods = new String[]{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE", "QUERY"};
for (String httpMethod : methods) {
RequestMethod requestMethod = RequestMethod.resolve(httpMethod);
assertThat(requestMethod).isNotNull();
diff --git a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java
index 5d33f8266b7d..08109a40ac84 100644
--- a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java
+++ b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java
@@ -75,7 +75,7 @@ void tearDown() throws Exception {
private MockResponse getRequest(RecordedRequest request, byte[] body, @Nullable String contentType) {
if (request.getMethod().equals("OPTIONS")) {
- return new MockResponse.Builder().code(200).setHeader("Allow", "GET, OPTIONS, HEAD, TRACE").build();
+ return new MockResponse.Builder().code(200).setHeader("Allow", "GET, QUERY, OPTIONS, HEAD, TRACE").build();
}
Buffer buf = new Buffer();
buf.write(body);
@@ -240,6 +240,29 @@ private MockResponse putRequest(RecordedRequest request, String expectedRequestC
return new MockResponse.Builder().code(202).build();
}
+ private MockResponse queryRequest(RecordedRequest request, String expectedRequestContent,
+ String contentType, byte[] responseBody) {
+
+ assertThat(request.getHeaders().values(CONTENT_LENGTH)).hasSize(1);
+ assertThat(Integer.parseInt(request.getHeaders().get(CONTENT_LENGTH))).as("Invalid request content-length").isGreaterThan(0);
+ String requestContentType = request.getHeaders().get(CONTENT_TYPE);
+ assertThat(requestContentType).as("No content-type").isNotNull();
+ Charset charset = StandardCharsets.ISO_8859_1;
+ if (requestContentType.contains("charset=")) {
+ String charsetName = requestContentType.split("charset=")[1];
+ charset = Charset.forName(charsetName);
+ }
+ assertThat(request.getBody().string(charset)).as("Invalid request body").isEqualTo(expectedRequestContent);
+ Buffer buf = new Buffer();
+ buf.write(responseBody);
+ return new MockResponse.Builder()
+ .code(200)
+ .setHeader(CONTENT_TYPE, contentType)
+ .setHeader(CONTENT_LENGTH, responseBody.length)
+ .body(buf)
+ .build();
+ }
+
protected class TestDispatcher extends Dispatcher {
@@ -302,6 +325,9 @@ else if (request.getTarget().equals("/patch")) {
else if (request.getTarget().equals("/put")) {
return putRequest(request, helloWorld);
}
+ else if (request.getTarget().equals("/query")) {
+ return queryRequest(request, helloWorld, textContentType.toString(), helloWorldBytes);
+ }
return new MockResponse.Builder().code(404).build();
}
catch (Throwable ex) {
diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java
index 6a99e795b751..d388184c9f01 100644
--- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java
+++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java
@@ -295,7 +295,7 @@ void optionsForAllow(ClientHttpRequestFactory clientHttpRequestFactory) {
setUpClient(clientHttpRequestFactory);
Set allowed = template.optionsForAllow(URI.create(baseUrl + "/get"));
- assertThat(allowed).as("Invalid response").isEqualTo(Set.of(HttpMethod.GET, HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.TRACE));
+ assertThat(allowed).as("Invalid response").isEqualTo(Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.TRACE));
}
@ParameterizedRestTemplateTest
diff --git a/spring-web/src/test/java/org/springframework/web/filter/FormContentFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/FormContentFilterTests.java
index d28c1da1e9ac..f016d119d1e8 100644
--- a/spring-web/src/test/java/org/springframework/web/filter/FormContentFilterTests.java
+++ b/spring-web/src/test/java/org/springframework/web/filter/FormContentFilterTests.java
@@ -58,14 +58,14 @@ void setup() {
@Test
- void wrapPutPatchAndDeleteOnly() throws Exception {
+ void wrapPutPatchQueryAndDeleteOnly() throws Exception {
for (HttpMethod method : HttpMethod.values()) {
MockHttpServletRequest request = new MockHttpServletRequest(method.name(), "/");
request.setContent("foo=bar".getBytes(StandardCharsets.ISO_8859_1));
request.setContentType("application/x-www-form-urlencoded; charset=ISO-8859-1");
this.filterChain = new MockFilterChain();
this.filter.doFilter(request, this.response, this.filterChain);
- if (method == HttpMethod.PUT || method == HttpMethod.PATCH || method == HttpMethod.DELETE) {
+ if (method == HttpMethod.PUT || method == HttpMethod.PATCH || method == HttpMethod.DELETE || method == HttpMethod.QUERY) {
assertThat(this.filterChain.getRequest()).isNotSameAs(request);
}
else {
diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/MvcAnnotationPredicates.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/MvcAnnotationPredicates.java
index fb106c77c25b..83ce9de8bb18 100644
--- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/MvcAnnotationPredicates.java
+++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/MvcAnnotationPredicates.java
@@ -114,7 +114,6 @@ public static RequestMappingPredicate headMapping(String... path) {
}
-
public static class ModelAttributePredicate implements Predicate {
private String name;
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java
index 5f6c24b17985..452428b250e7 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java
@@ -65,9 +65,12 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
private static final Method HTTP_OPTIONS_HANDLE_METHOD;
+ private static final Method HTTP_HEAD_QUERY_HANDLE_METHOD;
+
static {
try {
HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle");
+ HTTP_HEAD_QUERY_HANDLE_METHOD = HttpHeadQueryHandler.class.getMethod("handle");
}
catch (NoSuchMethodException ex) {
// Should never happen
@@ -189,10 +192,16 @@ protected void handleMatch(RequestMappingInfo info, HandlerMethod handlerMethod,
HttpMethod httpMethod = request.getMethod();
Set methods = helper.getAllowedMethods();
if (HttpMethod.OPTIONS.equals(httpMethod)) {
- Set mediaTypes = helper.getConsumablePatchMediaTypes();
- HttpOptionsHandler handler = new HttpOptionsHandler(methods, mediaTypes);
+ Set patchMediaTypes = helper.getConsumablePatchMediaTypes();
+ Set queryMediaTypes = helper.getConsumableQueryMediaTypes();
+ HttpOptionsHandler handler = new HttpOptionsHandler(methods, patchMediaTypes, queryMediaTypes);
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
}
+ if (HttpMethod.HEAD.equals(httpMethod) && methods.contains(HttpMethod.QUERY)) {
+ Set queryMediaTypes = helper.getConsumableQueryMediaTypes();
+ HttpHeadQueryHandler handler = new HttpHeadQueryHandler(methods, queryMediaTypes);
+ return new HandlerMethod(handler, HTTP_HEAD_QUERY_HANDLE_METHOD);
+ }
throw new MethodNotAllowedException(httpMethod, methods);
}
@@ -323,14 +332,23 @@ public List>> getParamConditions() {
* PATCH specified, or that have no methods at all.
*/
public Set getConsumablePatchMediaTypes() {
- Set result = new LinkedHashSet<>();
- for (PartialMatch match : this.partialMatches) {
- Set methods = match.getInfo().getMethodsCondition().getMethods();
- if (methods.isEmpty() || methods.contains(RequestMethod.PATCH)) {
- result.addAll(match.getInfo().getConsumesCondition().getConsumableMediaTypes());
- }
- }
- return result;
+ return getConsumableMediaTypesForMethod(RequestMethod.PATCH);
+ }
+
+ /**
+ * Return declared "consumable" types but only among those that have
+ * PATCH specified, or that have no methods at all.
+ */
+ public Set getConsumableQueryMediaTypes() {
+ return getConsumableMediaTypesForMethod(RequestMethod.QUERY);
+ }
+
+ private Set getConsumableMediaTypesForMethod(RequestMethod method) {
+ return this.partialMatches.stream()
+ .map(PartialMatch::getInfo)
+ .filter(info -> info.getMethodsCondition().getMethods().isEmpty() || info.getMethodsCondition().getMethods().contains(method))
+ .flatMap(info -> info.getConsumesCondition().getConsumableMediaTypes().stream())
+ .collect(Collectors.toCollection(LinkedHashSet::new));
}
@@ -400,9 +418,10 @@ private static class HttpOptionsHandler {
private final HttpHeaders headers = new HttpHeaders();
- public HttpOptionsHandler(Set declaredMethods, Set acceptPatch) {
+ public HttpOptionsHandler(Set declaredMethods, Set acceptPatch, Set acceptQuery) {
this.headers.setAllow(initAllowedHttpMethods(declaredMethods));
this.headers.setAcceptPatch(new ArrayList<>(acceptPatch));
+ this.headers.setAcceptQuery(new ArrayList<>(acceptQuery));
}
private static Set initAllowedHttpMethods(Set declaredMethods) {
@@ -413,7 +432,7 @@ private static Set initAllowedHttpMethods(Set declaredMe
}
else {
Set result = new LinkedHashSet<>(declaredMethods);
- if (result.contains(HttpMethod.GET)) {
+ if (result.contains(HttpMethod.GET) || result.contains(HttpMethod.QUERY)) {
result.add(HttpMethod.HEAD);
}
result.add(HttpMethod.OPTIONS);
@@ -427,4 +446,22 @@ public HttpHeaders handle() {
}
}
+ /**
+ * Default handler for HTTP HEAD targeting a QUERY endpoint.
+ */
+ private static class HttpHeadQueryHandler {
+
+ private final HttpHeaders headers = new HttpHeaders();
+
+ public HttpHeadQueryHandler(Set declaredMethods, Set acceptQuery) {
+ this.headers.setAllow(HttpOptionsHandler.initAllowedHttpMethods(declaredMethods));
+ this.headers.setAcceptQuery(new ArrayList<>(acceptQuery));
+ }
+
+ @SuppressWarnings("unused")
+ public HttpHeaders handle() {
+ return this.headers;
+ }
+ }
+
}
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java
index 7d8114d50f78..9b2a7ace68ba 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java
@@ -74,7 +74,7 @@
public abstract class AbstractMessageReaderArgumentResolver extends HandlerMethodArgumentResolverSupport {
private static final Set SUPPORTED_METHODS =
- Set.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
+ Set.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.QUERY);
private final List> messageReaders;
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
index bd8c0e13e68f..30454eaeba60 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
@@ -195,6 +195,10 @@ void getHandlerHttpOptions() {
testHttpOptions("/something", Set.of(HttpMethod.PUT, HttpMethod.POST), null);
testHttpOptions("/qux", Set.of(HttpMethod.PATCH,HttpMethod.GET,HttpMethod.HEAD,HttpMethod.OPTIONS),
new MediaType("foo", "bar"));
+ testHttpOptions("/quid", Set.of(HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.OPTIONS),
+ new MediaType("application", "json"));
+ testHttpHeadQuery("/quid", Set.of(HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.OPTIONS),
+ new MediaType("application", "json"));
}
@Test
@@ -377,7 +381,7 @@ private void testHttpMediaTypeNotSupportedException(String url) {
.isEqualTo(Collections.singletonList(new MediaType("application", "xml"))));
}
- private void testHttpOptions(String requestURI, Set allowedMethods, @Nullable MediaType acceptPatch) {
+ private void testHttpOptions(String requestURI, Set allowedMethods, @Nullable MediaType acceptMediaType) {
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.options(requestURI));
HandlerMethod handlerMethod = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
@@ -395,8 +399,36 @@ private void testHttpOptions(String requestURI, Set allowedMethods,
HttpHeaders headers = (HttpHeaders) value;
assertThat(headers.getAllow()).hasSameElementsAs(allowedMethods);
- if (acceptPatch != null && headers.getAllow().contains(HttpMethod.PATCH) ) {
- assertThat(headers.getAcceptPatch()).containsExactly(acceptPatch);
+ if (acceptMediaType != null) {
+ if (headers.getAllow().contains(HttpMethod.PATCH)) {
+ assertThat(headers.getAcceptPatch()).containsExactly(acceptMediaType);
+ }
+ if (headers.getAllow().contains(HttpMethod.QUERY)) {
+ assertThat(headers.getAcceptQuery()).containsExactly(acceptMediaType);
+ }
+ }
+ }
+
+ private void testHttpHeadQuery(String requestURI, Set allowedMethods, @Nullable MediaType acceptMediaType) {
+ ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.head(requestURI));
+ HandlerMethod handlerMethod = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
+
+ BindingContext bindingContext = new BindingContext();
+ InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
+ Mono mono = invocable.invoke(exchange, bindingContext);
+
+ HandlerResult result = mono.block();
+ assertThat(result).isNotNull();
+
+ Object value = result.getReturnValue();
+ assertThat(value).isNotNull();
+ assertThat(value.getClass()).isEqualTo(HttpHeaders.class);
+
+ HttpHeaders headers = (HttpHeaders) value;
+ assertThat(headers.getAllow()).hasSameElementsAs(allowedMethods);
+
+ if (acceptMediaType != null) {
+ assertThat(headers.getAcceptQuery()).containsExactly(acceptMediaType);
}
}
@@ -492,6 +524,11 @@ public String getBaz() {
public void patchBaz(String value) {
}
+ @RequestMapping(value = "/quid", method = RequestMethod.QUERY, consumes = "application/json", produces = "application/json")
+ public String query(@RequestBody String body) {
+ return "{}";
+ }
+
public void dummy() { }
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java
index 0856a0db021d..0b5e366c0cb1 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java
@@ -69,9 +69,12 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
private static final Method HTTP_OPTIONS_HANDLE_METHOD;
+ private static final Method HTTP_HEAD_QUERY_HANDLE_METHOD;
+
static {
try {
HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle");
+ HTTP_HEAD_QUERY_HANDLE_METHOD = HttpHeadQueryHandler.class.getMethod("handle");
}
catch (NoSuchMethodException ex) {
// Should never happen
@@ -253,10 +256,16 @@ private Map> extractMatrixVariables(
if (helper.hasMethodsMismatch()) {
Set methods = helper.getAllowedMethods();
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
- Set mediaTypes = helper.getConsumablePatchMediaTypes();
- HttpOptionsHandler handler = new HttpOptionsHandler(methods, mediaTypes);
+ Set patchMediaTypes = helper.getConsumablePatchMediaTypes();
+ Set queryMediaTypes = helper.getConsumableQueryMediaTypes();
+ HttpOptionsHandler handler = new HttpOptionsHandler(methods, patchMediaTypes, queryMediaTypes);
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
}
+ if (HttpMethod.HEAD.matches(request.getMethod()) && methods.contains(HttpMethod.QUERY.name())) {
+ Set queryMediaTypes = helper.getConsumableQueryMediaTypes();
+ HttpHeadQueryHandler handler = new HttpHeadQueryHandler(methods, queryMediaTypes);
+ return new HandlerMethod(handler, HTTP_HEAD_QUERY_HANDLE_METHOD);
+ }
throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
}
@@ -437,6 +446,21 @@ public Set getConsumablePatchMediaTypes() {
return result;
}
+ /**
+ * Return declared "consumable" types but only among those that have
+ * QUERY specified, or that have no methods at all.
+ */
+ public Set getConsumableQueryMediaTypes() {
+ Set result = new LinkedHashSet<>();
+ for (PartialMatch match : this.partialMatches) {
+ Set methods = match.getInfo().getMethodsCondition().getMethods();
+ if (methods.isEmpty() || methods.contains(RequestMethod.QUERY)) {
+ result.addAll(match.getInfo().getConsumesCondition().getConsumableMediaTypes());
+ }
+ }
+ return result;
+ }
+
/**
* Container for a RequestMappingInfo that matches the URL path at least.
@@ -501,9 +525,10 @@ private static class HttpOptionsHandler {
private final HttpHeaders headers = new HttpHeaders();
- public HttpOptionsHandler(Set declaredMethods, Set acceptPatch) {
+ public HttpOptionsHandler(Set declaredMethods, Set acceptPatch, Set acceptQuery) {
this.headers.setAllow(initAllowedHttpMethods(declaredMethods));
this.headers.setAcceptPatch(new ArrayList<>(acceptPatch));
+ this.headers.setAcceptQuery(new ArrayList<>(acceptQuery));
}
private static Set initAllowedHttpMethods(Set declaredMethods) {
@@ -519,7 +544,7 @@ private static Set initAllowedHttpMethods(Set declaredMethod
for (String method : declaredMethods) {
HttpMethod httpMethod = HttpMethod.valueOf(method);
result.add(httpMethod);
- if (httpMethod == HttpMethod.GET) {
+ if (httpMethod == HttpMethod.GET || httpMethod == HttpMethod.QUERY) {
result.add(HttpMethod.HEAD);
}
}
@@ -534,4 +559,22 @@ public HttpHeaders handle() {
}
}
+ /**
+ * Default handler for HTTP HEAD targeting a QUERY endpoint.
+ */
+ private static class HttpHeadQueryHandler {
+
+ private final HttpHeaders headers = new HttpHeaders();
+
+ public HttpHeadQueryHandler(Set declaredMethods, Set acceptQuery) {
+ this.headers.setAllow(HttpOptionsHandler.initAllowedHttpMethods(declaredMethods));
+ this.headers.setAcceptQuery(new ArrayList<>(acceptQuery));
+ }
+
+ @SuppressWarnings("unused")
+ public HttpHeaders handle() {
+ return this.headers;
+ }
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java
index 68c129a5dbde..f1f1c7e11f0e 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java
@@ -74,7 +74,7 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
protected enum ConverterType { BASE, GENERIC, SMART };
- private static final Set SUPPORTED_METHODS = Set.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
+ private static final Set SUPPORTED_METHODS = Set.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.QUERY);
private static final Object NO_VALUE = new Object();
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java
index 80d72452312e..b9f4c1adf49d 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java
@@ -123,7 +123,7 @@ void excludeMultipleMethods() {
testHttpMethods(
new HttpMethod[] {},
new HttpMethod[] {HttpMethod.GET, HttpMethod.POST, HttpMethod.OPTIONS},
- "HEAD", "PUT", "DELETE", "TRACE", "PATCH");
+ "HEAD", "PUT", "DELETE", "TRACE", "PATCH", "QUERY");
}
private void testHttpMethods(HttpMethod[] include, HttpMethod[] exclude, String... expected) {
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java
index 8ca35f451ff2..d2937784e700 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java
@@ -193,9 +193,11 @@ void getHandlerMediaTypeNotSupportedWithParseError(TestRequestMappingInfoHandler
void getHandlerHttpOptions(TestRequestMappingInfoHandlerMapping mapping) throws Exception {
testHttpOptions(mapping, "/foo", "GET,HEAD,OPTIONS", null);
testHttpOptions(mapping, "/person/1", "PUT,OPTIONS", null);
- testHttpOptions(mapping, "/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS", null);
+ testHttpOptions(mapping, "/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,QUERY", null);
testHttpOptions(mapping, "/something", "PUT,POST", null);
testHttpOptions(mapping, "/qux", "PATCH,GET,HEAD,OPTIONS", new MediaType("foo", "bar"));
+ testHttpOptions(mapping, "/quid", "QUERY,HEAD,OPTIONS", null);
+ testHttpHeadQuery(mapping, "/quid", "QUERY,HEAD,OPTIONS", MediaType.APPLICATION_JSON);
}
@PathPatternsParameterizedTest
@@ -477,6 +479,29 @@ private void testHttpOptions(TestRequestMappingInfoHandlerMapping mapping, Strin
}
}
+ private void testHttpHeadQuery(TestRequestMappingInfoHandlerMapping mapping, String requestURI,
+ String allowHeader, @Nullable MediaType acceptQuery) throws Exception {
+
+ MockHttpServletRequest request = new MockHttpServletRequest("HEAD", requestURI);
+ HandlerMethod handlerMethod = getHandler(mapping, request);
+
+ ServletWebRequest webRequest = new ServletWebRequest(request);
+ ModelAndViewContainer mavContainer = new ModelAndViewContainer();
+ Object result = new InvocableHandlerMethod(handlerMethod).invokeForRequest(webRequest, mavContainer);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getClass()).isEqualTo(HttpHeaders.class);
+ HttpHeaders headers = (HttpHeaders) result;
+ Set allowedMethods = Arrays.stream(allowHeader.split(","))
+ .map(HttpMethod::valueOf)
+ .collect(Collectors.toSet());
+ assertThat(headers.getAllow()).hasSameElementsAs(allowedMethods);
+
+ if (acceptQuery != null) {
+ assertThat(headers.getAcceptQuery()).containsExactly(acceptQuery);
+ }
+ }
+
private void testHttpMediaTypeNotAcceptableException(TestRequestMappingInfoHandlerMapping mapping, String url) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", url);
request.addHeader("Accept", "application/json");
@@ -572,6 +597,11 @@ public String getBaz() {
@RequestMapping(value = "/qux", method = RequestMethod.PATCH, consumes = "foo/bar")
public void patchBaz(String value) {
}
+
+ @RequestMapping(value = "/quid", method = RequestMethod.QUERY, consumes = "application/json", produces = "application/json")
+ public String query(@RequestBody String body) {
+ return "{}";
+ }
}
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java
index c07a3ed8a184..9f303d65522e 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java
@@ -148,6 +148,22 @@ void patchHttpMediaTypeNotSupported() {
assertThat(headers.getFirst(HttpHeaders.ACCEPT_PATCH)).isEqualTo("application/atom+xml, application/xml");
}
+ @Test
+ void queryHttpMediaTypeNotSupported() {
+ this.servletRequest = new MockHttpServletRequest("QUERY", "/");
+ this.request = new ServletWebRequest(this.servletRequest, this.servletResponse);
+
+ ResponseEntity