diff --git a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java
index 4a9fd9bc6114..cc3713e280e1 100644
--- a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java
+++ b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java
@@ -184,6 +184,16 @@ public static BaseBuilder> options(String urlTemplate, @Nullable Object... uri
return method(HttpMethod.OPTIONS, urlTemplate, uriVars);
}
+ /**
+ * HTTP QUERY variant. See {@link #get(String, Object...)} for general info.
+ * @param urlTemplate a URL template; the resulting URL will be encoded
+ * @param uriVars zero or more URI variables
+ * @return the created builder
+ */
+ public static BodyBuilder query(String urlTemplate, @Nullable Object... uriVars) {
+ return method(HttpMethod.QUERY, urlTemplate, uriVars);
+ }
+
/**
* Create a builder with the given HTTP method and a {@link URI}.
* @param method the HTTP method (GET, POST, etc)
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
index 5109efe2ef57..7d1c55fbc2bd 100644
--- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
@@ -167,6 +167,11 @@ public RequestHeadersUriSpec> options() {
return methodInternal(HttpMethod.OPTIONS);
}
+ @Override
+ public RequestBodyUriSpec query() {
+ return methodInternal(HttpMethod.QUERY);
+ }
+
@Override
public RequestBodyUriSpec method(HttpMethod httpMethod) {
return methodInternal(httpMethod);
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
index 7a3b1e75e171..9bdcfb77a34e 100644
--- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
@@ -146,6 +146,13 @@ public interface WebTestClient {
*/
RequestHeadersUriSpec> options();
+ /**
+ * Prepare an HTTP QUERY request.
+ * @return a spec for specifying the target URL
+ * @since 7.1
+ */
+ RequestBodyUriSpec query();
+
/**
* Prepare a request for the specified {@code HttpMethod}.
* @return a spec for specifying the target URL
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java
index c90719e8be4d..b61389ace0a8 100644
--- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java
@@ -333,6 +333,21 @@ public MockMvcRequestBuilder options() {
return method(HttpMethod.OPTIONS);
}
+ /**
+ * Prepare an HTTP QUERY request.
+ *
The returned builder can be wrapped in {@code assertThat} to enable
+ * assertions on the result. For multi-statements assertions, use
+ * {@link MockMvcRequestBuilder#exchange() exchange()} to assign the
+ * result. To control the time to wait for asynchronous request to complete
+ * on a per-request basis, use
+ * {@link MockMvcRequestBuilder#exchange(Duration) exchange(Duration)}.
+ * @return a request builder for specifying the target URI
+ * @since 7.1
+ */
+ public MockMvcRequestBuilder query() {
+ return method(HttpMethod.QUERY);
+ }
+
/**
* Prepare a request for the specified {@code HttpMethod}.
*
The returned builder can be wrapped in {@code assertThat} to enable
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java
index 1fc9f45bb325..add30502e4d3 100644
--- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java
@@ -119,6 +119,11 @@ public RequestHeadersUriSpec> delete() {
return methodInternal(HttpMethod.DELETE);
}
+ @Override
+ public RequestBodyUriSpec query() {
+ return methodInternal(HttpMethod.QUERY);
+ }
+
@Override
public RequestHeadersUriSpec> options() {
return methodInternal(HttpMethod.OPTIONS);
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/RestTestClient.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/RestTestClient.java
index c37d53c349bd..982fb5151615 100644
--- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/RestTestClient.java
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/RestTestClient.java
@@ -119,6 +119,13 @@ public interface RestTestClient {
*/
RequestHeadersUriSpec> delete();
+ /**
+ * Prepare an HTTP QUERY request.
+ * @return a spec for specifying the target URL
+ * @since 7.1
+ */
+ RequestBodyUriSpec query();
+
/**
* Prepare an HTTP OPTIONS request.
* @return a spec for specifying the target URL
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java
index bfef763d183a..c61894de43e3 100644
--- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java
@@ -175,6 +175,25 @@ public static MockHttpServletRequestBuilder head(URI uri) {
return new MockHttpServletRequestBuilder(HttpMethod.HEAD).uri(uri);
}
+ /**
+ * Create a {@link MockHttpServletRequestBuilder} for a QUERY request.
+ * @param uriTemplate a URI template; the resulting URI will be encoded
+ * @param uriVariables zero or more URI variables
+ * @since 7.1
+ */
+ public static MockHttpServletRequestBuilder query(String uriTemplate, @Nullable Object... uriVariables) {
+ return new MockHttpServletRequestBuilder(HttpMethod.QUERY).uri(uriTemplate, uriVariables);
+ }
+
+ /**
+ * Create a {@link MockHttpServletRequestBuilder} for a QUERY request.
+ * @param uri the URI
+ * @since 7.1
+ */
+ public static MockHttpServletRequestBuilder query(URI uri) {
+ return new MockHttpServletRequestBuilder(HttpMethod.QUERY).uri(uri);
+ }
+
/**
* Create a {@link MockHttpServletRequestBuilder} for a request with the given HTTP method.
* @param method the HTTP method (GET, POST, etc.)
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..3329ac1c160c 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
@@ -57,7 +57,7 @@ class RestTestClientTests {
class HttpMethods {
@ParameterizedTest
- @ValueSource(strings = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"})
+ @ValueSource(strings = {"GET", "POST", "PUT", "DELETE", "PATCH", "QUERY", "HEAD"})
void method(String method) {
RestTestClientTests.this.client.method(HttpMethod.valueOf(method)).uri("/test")
.exchange()
@@ -118,10 +118,18 @@ 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();
}
+ @Test
+ void testQuery() {
+ RestTestClientTests.this.client.query().uri("/test")
+ .exchange()
+ .expectStatus().isOk()
+ .expectBody().jsonPath("$.method").isEqualTo("QUERY");
+ }
+
}
diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java
index 3a20b9efb17b..aef9db9753bc 100644
--- a/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java
+++ b/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java
@@ -31,6 +31,8 @@
import jakarta.servlet.http.Cookie;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -418,18 +420,21 @@ void requestParameterFromMultiValueMap() {
assertThat(request.getParameterMap().get("foo")).containsExactly("bar", "baz");
}
- @Test
- void requestParameterFromRequestBodyFormData() {
+ @ValueSource(strings = {"POST", "QUERY"})
+ @ParameterizedTest()
+ void requestParameterFromRequestBodyFormData(String methodName) {
String contentType = "application/x-www-form-urlencoded;charset=UTF-8";
String body = "name+1=value+1&name+2=value+A&name+2=value+B&name+3";
- MockHttpServletRequest request = new MockHttpServletRequestBuilder(POST).uri("/foo")
+ HttpMethod method = HttpMethod.valueOf(methodName);
+ MockHttpServletRequest request = new MockHttpServletRequestBuilder(method).uri("/foo")
.contentType(contentType).content(body.getBytes(UTF_8))
.buildRequest(this.servletContext);
assertThat(request.getParameterMap().get("name 1")).containsExactly("value 1");
assertThat(request.getParameterMap().get("name 2")).containsExactly("value A", "value B");
assertThat(request.getParameterMap().get("name 3")).containsExactly((String) null);
+
}
@Test
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..969bbf85d573 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
@@ -129,6 +129,13 @@ public class HttpHeaders implements Serializable {
* @see Section 5.3.5 of RFC 7233
*/
public static final String ACCEPT_RANGES = "Accept-Ranges";
+
+ /**
+ * 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 CORS {@code Access-Control-Allow-Credentials} response header field name.
* @see CORS W3C recommendation
@@ -648,6 +655,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/RequestEntity.java b/spring-web/src/main/java/org/springframework/http/RequestEntity.java
index d9badcb313e2..610aeae381c7 100644
--- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java
+++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java
@@ -384,6 +384,27 @@ public static BodyBuilder post(String uriTemplate, @Nullable Object... uriVariab
return method(HttpMethod.POST, uriTemplate, uriVariables);
}
+ /**
+ * Create an HTTP QUERY builder with the given url.
+ * @param url the URL
+ * @return the created builder
+ * @since 7.1
+ */
+ public static BodyBuilder query(URI url) {
+ return method(HttpMethod.QUERY, url);
+ }
+
+ /**
+ * Create an HTTP QUERY builder with the given string base uri template.
+ * @param uriTemplate the uri template to use
+ * @param uriVariables variables to expand the URI template with
+ * @return the created builder
+ * @since 7.1
+ */
+ public static BodyBuilder query(String uriTemplate, Object... uriVariables) {
+ return method(HttpMethod.QUERY, uriTemplate, uriVariables);
+ }
+
/**
* Create an HTTP PUT builder with the given url.
* @param url the URL
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/http/client/SimpleClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java
index da28a171756d..8fb5f0e47c29 100644
--- a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java
+++ b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java
@@ -155,7 +155,8 @@ protected void prepareConnection(HttpURLConnection connection, String httpMethod
boolean mayWrite =
("POST".equals(httpMethod) || "PUT".equals(httpMethod) ||
- "PATCH".equals(httpMethod) || "DELETE".equals(httpMethod));
+ "PATCH".equals(httpMethod) || "DELETE".equals(httpMethod) ||
+ "QUERY".equals(httpMethod));
connection.setDoInput(true);
connection.setInstanceFollowRedirects("GET".equals(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..03fa71c85e2a 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());
}
+ if (HttpMethod.QUERY.equals(this.httpMethod)) {
+ headers.setAcceptQuery(getSupportedMediaTypes());
+ }
return headers;
}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java
index 8ffd88ba6bd3..33eb87aaed4b 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java
@@ -44,6 +44,7 @@
* @see PostMapping
* @see PutMapping
* @see PatchMapping
+ * @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java
index 121813ff2557..1a408f0f1826 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java
@@ -44,6 +44,7 @@
* @see PutMapping
* @see DeleteMapping
* @see PatchMapping
+ * @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java
index c6f4d0ea846f..bdd8b5c082e3 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java
@@ -44,6 +44,7 @@
* @see PostMapping
* @see PutMapping
* @see DeleteMapping
+ * @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java
index 4f2121b95194..95be30936b45 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java
@@ -44,6 +44,7 @@
* @see PutMapping
* @see DeleteMapping
* @see PatchMapping
+ * @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java
index 56f1bac2760c..1c2482a9617a 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java
@@ -44,6 +44,7 @@
* @see PostMapping
* @see DeleteMapping
* @see PatchMapping
+ * @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java
new file mode 100644
index 000000000000..e794b85956dd
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2026-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.web.bind.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.core.annotation.AliasFor;
+
+/**
+ * Annotation for mapping HTTP {@code QUERY} requests onto specific handler
+ * methods.
+ *
+ *
Specifically, {@code @QueryMapping} is a composed annotation that
+ * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.QUERY)}.
+ *
+ *
NOTE: This annotation cannot be used in conjunction with
+ * other {@code @RequestMapping} annotations that are declared on the same method.
+ * If multiple {@code @RequestMapping} annotations are detected on the same method,
+ * a warning will be logged, and only the first mapping will be used. This applies
+ * to {@code @RequestMapping} as well as composed {@code @RequestMapping} annotations
+ * such as {@code @GetMapping}, {@code @PutMapping}, etc.
+ *
+ * @author Mario Ruiz
+ * @since 7.1
+ * @see GetMapping
+ * @see PutMapping
+ * @see PostMapping
+ * @see DeleteMapping
+ * @see PatchMapping
+ * @see RequestMapping
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@RequestMapping(method = RequestMethod.QUERY)
+public @interface QueryMapping{
+
+ /**
+ * Alias for {@link RequestMapping#name}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String name() default "";
+
+ /**
+ * Alias for {@link RequestMapping#value}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] value() default {};
+
+ /**
+ * Alias for {@link RequestMapping#path}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] path() default {};
+
+ /**
+ * Alias for {@link RequestMapping#params}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] params() default {};
+
+ /**
+ * Alias for {@link RequestMapping#headers}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] headers() default {};
+
+ /**
+ * Alias for {@link RequestMapping#consumes}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] consumes() default {};
+
+ /**
+ * Alias for {@link RequestMapping#produces}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] produces() default {};
+
+ /**
+ * Alias for {@link RequestMapping#version()}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String version() default "";
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
index 07f994561780..d01073fe8eb1 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
@@ -51,8 +51,8 @@
* at the method level. In most cases, at the method level applications will
* prefer to use one of the HTTP method specific variants
* {@link GetMapping @GetMapping}, {@link PostMapping @PostMapping},
- * {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping}, or
- * {@link PatchMapping @PatchMapping}.
+ * {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping},
+ * {@link PatchMapping @PatchMapping}, or {@link QueryMapping @QueryMapping}.
*
*
NOTE: This annotation cannot be used in conjunction with
* other {@code @RequestMapping} annotations that are declared on the same element
@@ -75,6 +75,7 @@
* @see PutMapping
* @see DeleteMapping
* @see PatchMapping
+ * @see QueryMapping
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@@ -125,7 +126,7 @@
/**
* The HTTP request methods to map to, narrowing the primary mapping:
- * GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
+ * GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE, QUERY.
*
Supported at the type level as well as at the method level!
* When used at the type level, all method-level mappings inherit this
* HTTP method restriction.
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/client/DefaultRestClient.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java
index 0b937410d14b..e7fdf92d7ccc 100644
--- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java
+++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java
@@ -194,6 +194,11 @@ public RequestHeadersUriSpec> options() {
return methodInternal(HttpMethod.OPTIONS);
}
+ @Override
+ public RequestBodyUriSpec query() {
+ return methodInternal(HttpMethod.QUERY);
+ }
+
@Override
public RequestBodyUriSpec method(HttpMethod method) {
Assert.notNull(method, "HttpMethod must not be null");
diff --git a/spring-web/src/main/java/org/springframework/web/client/RestClient.java b/spring-web/src/main/java/org/springframework/web/client/RestClient.java
index 22850d80070e..80f651017742 100644
--- a/spring-web/src/main/java/org/springframework/web/client/RestClient.java
+++ b/spring-web/src/main/java/org/springframework/web/client/RestClient.java
@@ -125,6 +125,13 @@ public interface RestClient {
*/
RequestHeadersUriSpec> options();
+ /**
+ * Start building an HTTP QUERY request.
+ * @return a spec for specifying the target URL
+ * @since 7.1
+ */
+ RequestBodyUriSpec query();
+
/**
* Start building a request for the given {@code HttpMethod}.
* @return a spec for specifying the target URL
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
index a408ae5fe297..799cb8fee731 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
@@ -50,7 +50,7 @@
*/
public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest {
- private static final Set SAFE_METHODS = Set.of("GET", "HEAD");
+ private static final Set SAFE_METHODS = Set.of("GET", "HEAD", "QUERY");
/**
* Date formats as specified in the HTTP RFC.
diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java
index f3864c6a2df6..8ecb2f4bd96a 100644
--- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java
+++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java
@@ -63,9 +63,9 @@ public class CorsConfiguration {
private static final List DEFAULT_PERMIT_ALL = Collections.singletonList(ALL);
- private static final List DEFAULT_METHODS = List.of(HttpMethod.GET, HttpMethod.HEAD);
+ private static final List DEFAULT_METHODS = List.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
- private static final List DEFAULT_PERMIT_METHODS = List.of(HttpMethod.GET.name(),
+ private static final List DEFAULT_PERMIT_METHODS = List.of(HttpMethod.GET.name(), HttpMethod.QUERY.name(),
HttpMethod.HEAD.name(), HttpMethod.POST.name());
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/ShallowEtagHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java
index 0ba14cbe48e9..32541d9154b8 100644
--- a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java
+++ b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java
@@ -151,7 +151,7 @@ protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletRespo
if (!response.isCommitted() &&
responseStatusCode >= 200 && responseStatusCode < 300 &&
- HttpMethod.GET.matches(request.getMethod())) {
+ (HttpMethod.GET.matches(request.getMethod()) || HttpMethod.QUERY.matches(request.getMethod()))) {
String cacheControl = response.getHeader(HttpHeaders.CACHE_CONTROL);
return (cacheControl == null || !cacheControl.contains(DIRECTIVE_NO_STORE));
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..62cb851198f8 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);
}
+ if (this.method == HttpMethod.QUERY) {
+ headers.setAcceptQuery(this.supportedMediaTypes);
+ }
return headers;
}
diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java
index 8e72fb6f79d5..fac1d1e3c3fa 100644
--- a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java
+++ b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java
@@ -66,7 +66,7 @@
*/
public class DefaultServerWebExchange implements ServerWebExchange {
- private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
+ private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
private static final ResolvableType FORM_DATA_TYPE =
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
diff --git a/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java b/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java
index 843ae4a8071a..c2696c82ece4 100644
--- a/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java
+++ b/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java
@@ -51,6 +51,7 @@
*
{@link PutExchange}
*
{@link PatchExchange}
*
{@link DeleteExchange}
+ *
{@link QueryExchange}
*
*
*
Supported method arguments:
diff --git a/spring-web/src/main/java/org/springframework/web/service/annotation/QueryExchange.java b/spring-web/src/main/java/org/springframework/web/service/annotation/QueryExchange.java
new file mode 100644
index 000000000000..44cf5cc48638
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/service/annotation/QueryExchange.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2024-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.web.service.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.core.annotation.AliasFor;
+
+
+/**
+ * Shortcut for {@link HttpExchange @HttpExchange} for HTTP QUERY requests.
+ *
+ * @author Mario Ruiz
+ * @since 7.1
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@HttpExchange(method = "QUERY")
+public @interface QueryExchange {
+ /**
+ * Alias for {@link HttpExchange#value}.
+ */
+ @AliasFor(annotation = HttpExchange.class)
+ String value() default "";
+
+ /**
+ * Alias for {@link HttpExchange#url()}.
+ */
+ @AliasFor(annotation = HttpExchange.class)
+ String url() default "";
+
+ /**
+ * Alias for {@link HttpExchange#contentType()}.
+ */
+ @AliasFor(annotation = HttpExchange.class)
+ String contentType() default "";
+
+ /**
+ * Alias for {@link HttpExchange#accept()}.
+ */
+ @AliasFor(annotation = HttpExchange.class)
+ String[] accept() default {};
+
+ /**
+ * Alias for {@link HttpExchange#headers()}.
+ */
+ @AliasFor(annotation = HttpExchange.class)
+ String[] headers() default {};
+
+ /**
+ * Alias for {@link HttpExchange#version()}.
+ */
+ @AliasFor(annotation = HttpExchange.class)
+ String version() default "";
+}
diff --git a/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt b/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt
index 2ed06fc368f3..338471226383 100644
--- a/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt
+++ b/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt
@@ -20,6 +20,7 @@ package org.springframework.web.client
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.HttpEntity
+import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.RequestEntity
import org.springframework.http.ResponseEntity
@@ -291,3 +292,44 @@ inline fun RestOperations.exchange(url: URI, method: HttpMetho
@Throws(RestClientException::class)
inline fun RestOperations.exchange(requestEntity: RequestEntity<*>): ResponseEntity =
exchange(requestEntity, object : ParameterizedTypeReference() {})
+
+/**
+ * Extension for [RestOperations] providing a `queryForEntity(...)`
+ * variant leveraging Kotlin reified type parameters. Like the original Java method, this
+ * extension is subject to type erasure. Use [exchange] if you need to retain actual
+ * generic type arguments.
+ *
+ * @author Mario Ruiz
+ * @since 7.1
+ */
+@Throws(RestClientException::class)
+inline fun RestOperations.queryForEntity(url: String, request: Any? = null,
+ vararg uriVariables: Any?): ResponseEntity =
+ exchange(url = url, method = HttpMethod.QUERY, requestEntity = HttpEntity(request, null as (HttpHeaders?) ), uriVariables= uriVariables)
+
+/**
+ * Extension for [RestOperations] providing a `queryForEntity(...)`
+ * variant leveraging Kotlin reified type parameters. Like the original Java method, this
+ * extension is subject to type erasure. Use [exchange] if you need to retain actual
+ * generic type arguments.
+ *
+ * @author Mario Ruiz
+ * @since 7.1
+ */
+@Throws(RestClientException::class)
+inline fun RestOperations.queryForEntity(url: String, request: Any? = null,
+ uriVariables: Map): ResponseEntity =
+ exchange(url = url, method = HttpMethod.QUERY, requestEntity = HttpEntity(request, null as (HttpHeaders?) ), uriVariables= uriVariables)
+
+/**
+ * Extension for [RestOperations] providing a `queryForEntity(...)`
+ * variant leveraging Kotlin reified type parameters. Like the original Java method, this
+ * extension is subject to type erasure. Use [exchange] if you need to retain actual
+ * generic type arguments.
+ *
+ * @author Mario Ruiz
+ * @since 7.1
+ */
+@Throws(RestClientException::class)
+inline fun RestOperations.queryForEntity(url: URI, request: Any? = null): ResponseEntity =
+ exchange(url = url, method = HttpMethod.QUERY, requestEntity = HttpEntity(request, null as (HttpHeaders?) ))
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/ControllerMappingReflectiveProcessorTests.java b/spring-web/src/test/java/org/springframework/web/bind/annotation/ControllerMappingReflectiveProcessorTests.java
index 83a03fddb20c..187df08a531a 100644
--- a/spring-web/src/test/java/org/springframework/web/bind/annotation/ControllerMappingReflectiveProcessorTests.java
+++ b/spring-web/src/test/java/org/springframework/web/bind/annotation/ControllerMappingReflectiveProcessorTests.java
@@ -213,6 +213,11 @@ Response get() {
void post(@RequestBody Request request) {
}
+ @QueryMapping
+ Response query(@RequestBody Request request) {
+ return new Response("response");
+ }
+
@PostMapping
void postForm(@ModelAttribute Request request) {
}
@@ -247,6 +252,17 @@ void postRawHttpEntity(HttpEntity entity) {
void postPartToConvert(@RequestPart Request request) {
}
+ @QueryMapping
+ HttpEntity querytHttpEntity(HttpEntity entity) {
+ return new HttpEntity<>(new Response("response"));
+ }
+
+ @QueryMapping
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ HttpEntity queryRawHttpEntity(HttpEntity entity) {
+ return new HttpEntity(new Response("response"));
+ }
+
}
@RestController
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/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
index cfeb32f892fe..c3c678c732ce 100644
--- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
+++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
@@ -42,6 +42,7 @@
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
+import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
@@ -73,6 +74,7 @@
import static org.springframework.http.HttpMethod.PATCH;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;
+import static org.springframework.http.HttpMethod.QUERY;
import static org.springframework.http.MediaType.parseMediaType;
/**
@@ -464,6 +466,46 @@ void postForEntityNull() throws Exception {
verify(response).close();
}
+ @Test
+ void queryForEntity() throws Exception {
+ mockTextPlainHttpMessageConverter();
+ HttpHeaders requestHeaders = new HttpHeaders();
+ mockSentRequest(QUERY, "https://example.com", requestHeaders);
+ mockResponseStatus(HttpStatus.OK);
+ String expected = "42";
+ mockResponseBody(expected, MediaType.TEXT_PLAIN);
+
+ ResponseEntity result = template.exchange(RequestEntity.query("https://example.com").body("Hello World"), String.class);
+ assertThat(result.getBody()).as("Invalid QUERY result").isEqualTo(expected);
+ assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN);
+ assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
+ assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);
+
+ verify(response).close();
+ }
+
+ @Test
+ void queryForEntityNull() throws Exception {
+ mockTextPlainHttpMessageConverter();
+ HttpHeaders requestHeaders = new HttpHeaders();
+ mockSentRequest(QUERY, "https://example.com", requestHeaders);
+ mockResponseStatus(HttpStatus.OK);
+ HttpHeaders responseHeaders = new HttpHeaders();
+ responseHeaders.setContentType(MediaType.TEXT_PLAIN);
+ responseHeaders.setContentLength(10);
+ given(response.getHeaders()).willReturn(responseHeaders);
+ given(response.getBody()).willReturn(InputStream.nullInputStream());
+ given(converter.read(String.class, response)).willReturn(null);
+
+ ResponseEntity result = template.exchange("https://example.com",QUERY, null, String.class);
+ assertThat(result.hasBody()).as("Invalid QUERY result").isFalse();
+ assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN);
+ assertThat(requestHeaders.getContentLength()).as("Invalid content length").isEqualTo(0);
+ assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);
+
+ verify(response).close();
+ }
+
@Test
void put() throws Exception {
mockTextPlainHttpMessageConverter();
diff --git a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java
index 787ccad785f2..df578184342f 100644
--- a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java
+++ b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java
@@ -323,7 +323,7 @@ private void assertOkWithLastModified(Instant lastModified) {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ParameterizedTest(name = "[{index}] {0}")
- @ValueSource(strings = {"GET", "HEAD"})
+ @ValueSource(strings = {"GET", "HEAD", "QUERY"})
@interface SafeHttpMethodsTest {
}
diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java
index 6bc920185474..9bfad9edc681 100644
--- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java
+++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java
@@ -140,7 +140,7 @@ void combineWithDefaultPermitValues() {
assertThat(config.getAllowedHeaders()).containsExactly("*");
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedMethods())
- .containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name());
+ .containsExactly(HttpMethod.GET.name(), HttpMethod.QUERY.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name());
assertThat(combinedConfig.getExposedHeaders()).isEmpty();
combinedConfig = new CorsConfiguration().combine(config);
@@ -148,7 +148,7 @@ void combineWithDefaultPermitValues() {
assertThat(config.getAllowedHeaders()).containsExactly("*");
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedMethods())
- .containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name());
+ .containsExactly(HttpMethod.GET.name(), HttpMethod.QUERY.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name());
assertThat(combinedConfig.getExposedHeaders()).isEmpty();
}
@@ -394,7 +394,9 @@ void checkOriginPatternNotAllowed() {
@Test
void checkMethodAllowed() {
CorsConfiguration config = new CorsConfiguration();
- assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET, HttpMethod.HEAD);
+ assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
+ assertThat(config.checkHttpMethod(HttpMethod.QUERY)).containsExactly(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
+
config.addAllowedMethod("GET");
assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET);
@@ -450,7 +452,7 @@ void changePermitDefaultValues() {
assertThat(config.getAllowedOrigins()).containsExactly("*", "https://domain.com");
assertThat(config.getAllowedHeaders()).containsExactly("*", "header1");
- assertThat(config.getAllowedMethods()).containsExactly("GET", "HEAD", "POST", "PATCH");
+ assertThat(config.getAllowedMethods()).containsExactly("GET", "QUERY", "HEAD", "POST", "PATCH");
}
@Test
diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java
index 7f667e5d1116..a8eb71fb7c00 100644
--- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java
+++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java
@@ -252,7 +252,7 @@ void preflightRequestMatchedAllowedMethod() throws Exception {
this.processor.processRequest(this.conf, this.request, this.response);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
- assertThat(this.response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,HEAD");
+ assertThat(this.response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,QUERY,HEAD");
assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN,
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
}
diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java
index e8f9e2becc89..7322e2dbaad6 100644
--- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java
+++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java
@@ -263,7 +263,7 @@ void preflightRequestMatchedAllowedMethod() {
assertThat(response.getStatusCode()).isNull();
assertThat(response.getHeaders().get(VARY)).contains(ORIGIN,
ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS);
- assertThat(response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,HEAD");
+ assertThat(response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,QUERY,HEAD");
}
@Test
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..d8043e463863 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
@@ -113,6 +113,10 @@ public static RequestMappingPredicate headMapping(String... path) {
return new RequestMappingPredicate(path).method(RequestMethod.HEAD);
}
+ public static RequestMappingPredicate queryMapping(String... path) {
+ return new RequestMappingPredicate(path).method(RequestMethod.QUERY);
+ }
+
public static class ModelAttributePredicate implements Predicate {
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java
index 3cc7f07e57d6..5c2c4628faec 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java
@@ -177,6 +177,11 @@ public RequestHeadersUriSpec> options() {
return methodInternal(HttpMethod.OPTIONS);
}
+ @Override
+ public RequestBodyUriSpec query() {
+ return methodInternal(HttpMethod.QUERY);
+ }
+
@Override
public RequestBodyUriSpec method(HttpMethod httpMethod) {
return methodInternal(httpMethod);
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java
index bf7714434489..143c6b45a086 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java
@@ -123,6 +123,13 @@ public interface WebClient {
*/
RequestHeadersUriSpec> options();
+ /**
+ * Start building an HTTP QUERY request.
+ * @return a spec for specifying the target URL
+ * @since 7.1
+ */
+ RequestBodyUriSpec query();
+
/**
* Start building a request for the given {@code HttpMethod}.
* @return a spec for specifying the target URL
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java
index 72692bdfa0bc..81e63348bba7 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java
@@ -288,7 +288,7 @@ public Mono render(String name, Map model) {
*/
abstract static class AbstractServerResponse implements ServerResponse {
- private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
+ private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
private final HttpStatusCode statusCode;
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java
index 24c3f48ab169..a8ab7e8e477c 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java
@@ -288,6 +288,19 @@ public static RequestPredicate OPTIONS(String pattern) {
return method(HttpMethod.OPTIONS).and(path(pattern));
}
+ /**
+ * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code QUERY}
+ * and the given {@code pattern} matches against the request path.
+ * @param pattern the path pattern to match against
+ * @return a predicate that matches if the request method is QUERY and if the given pattern
+ * matches against the request path
+ * @since 7.1
+ * @see org.springframework.web.util.pattern.PathPattern
+ */
+ public static RequestPredicate QUERY(String pattern) {
+ return method(HttpMethod.QUERY).and(path(pattern));
+ }
+
/**
* Return a {@code RequestPredicate} that matches if the request's path has the given extension.
* @param extension the path extension to match against, ignoring case
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java
index dc970358149e..0f56ba7050a4 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java
@@ -43,7 +43,7 @@
class ResourceHandlerFunction implements HandlerFunction {
private static final Set SUPPORTED_METHODS =
- Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS);
+ Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.OPTIONS);
private final Resource resource;
@@ -66,6 +66,12 @@ public Mono handle(ServerRequest request) {
.build()
.map(response -> response);
}
+ else if (HttpMethod.QUERY.equals(method)) {
+ return EntityResponse.fromObject(this.resource)
+ .headers(headers -> this.headersConsumer.accept(this.resource, headers))
+ .build()
+ .map(response -> response);
+ }
else if (HttpMethod.HEAD.equals(method)) {
Resource headResource = new HeadMethodResource(this.resource);
return EntityResponse.fromObject(headResource)
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java
index e704adbbf883..1d4427a2904b 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java
@@ -248,6 +248,30 @@ public RouterFunctions.Builder OPTIONS(
return add(RequestPredicates.OPTIONS(pattern).and(predicate), function);
}
+ // QUERY
+
+ @Override
+ public RouterFunctions.Builder QUERY(HandlerFunction handlerFunction) {
+ return add(RequestPredicates.method(HttpMethod.QUERY), handlerFunction);
+ }
+
+ @Override
+ public RouterFunctions.Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction) {
+ return add(RequestPredicates.method(HttpMethod.QUERY).and(predicate), handlerFunction);
+ }
+
+ @Override
+ public RouterFunctions.Builder QUERY(String pattern, HandlerFunction handlerFunction) {
+ return add(RequestPredicates.QUERY(pattern), handlerFunction);
+ }
+
+ @Override
+ public RouterFunctions.Builder QUERY(String pattern, RequestPredicate predicate,
+ HandlerFunction handlerFunction) {
+
+ return add(RequestPredicates.QUERY(pattern).and(predicate), handlerFunction);
+ }
+
// other
@Override
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java
index e1d469a64193..a74a5626e66c 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java
@@ -741,6 +741,59 @@ Builder OPTIONS(
Builder OPTIONS(
String pattern, RequestPredicate predicate, HandlerFunction handlerFunction);
+ /**
+ * Adds a route to the given handler function that handles HTTP {@code QUERY} requests.
+ * @param handlerFunction the handler function to handle all {@code QUERY} requests
+ * @return this builder
+ * @since 7.1
+ */
+ Builder QUERY(HandlerFunction handlerFunction);
+
+ /**
+ * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests
+ * that match the given pattern.
+ * @param pattern the pattern to match to
+ * @param handlerFunction the handler function to handle all {@code QUERY} requests that
+ * match {@code pattern}
+ * @return this builder
+ * @since 7.1
+ * @see org.springframework.web.util.pattern.PathPattern
+ */
+ Builder QUERY(String pattern, HandlerFunction handlerFunction);
+
+ /**
+ * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests
+ * that match the given predicate.
+ * @param predicate predicate to match
+ * @param handlerFunction the handler function to handle all {@code QUERY} requests that
+ * match {@code predicate}
+ * @return this builder
+ * @since 7.1
+ * @see RequestPredicates
+ */
+ Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction);
+
+ /**
+ * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests
+ * that match the given pattern and predicate.
+ *
For instance, the following example routes QUERY requests for "/user" that contain JSON
+ * to the {@code addUser} method in {@code userController}:
+ *
+ * @param pattern the pattern to match to
+ * @param predicate additional predicate to match
+ * @param handlerFunction the handler function to handle all {@code QUERY} requests that
+ * match {@code pattern}
+ * @return this builder
+ * @since 7.1
+ * @see org.springframework.web.util.pattern.PathPattern
+ */
+ Builder QUERY(String pattern, RequestPredicate predicate, HandlerFunction handlerFunction);
+
/**
* Adds a route to the given handler function that handles all
* requests that match the given predicate.
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java
index 50a2b9fd0c9d..2915b934bda2 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java
@@ -85,7 +85,7 @@
*/
public class ResourceWebHandler implements WebHandler, InitializingBean {
- private static final Set SUPPORTED_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
+ private static final Set SUPPORTED_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
private static final Log logger = LogFactory.getLog(ResourceWebHandler.class);
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..45a7532e55c3 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
@@ -189,8 +189,9 @@ 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);
}
throw new MethodNotAllowedException(httpMethod, methods);
@@ -323,14 +324,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 +410,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 +424,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);
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/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java
index abf65d2bbbe7..10eee4bffd7b 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java
@@ -56,7 +56,7 @@
*/
public class ResponseEntityResultHandler extends AbstractMessageWriterResultHandler implements HandlerResultHandler {
- private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
+ private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
/**
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java
index 62e37c50be5e..09c6fa27ea16 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java
@@ -729,7 +729,7 @@ private Consumer assertNotModified(@Nullable String eTag, @Nulla
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ParameterizedTest(name = "[{index}] {0}")
- @ValueSource(strings = {"GET", "HEAD"})
+ @ValueSource(strings = {"GET", "HEAD", "QUERY"})
@interface SafeHttpMethodsTest {
}
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java
index 7a2fbc496ad0..1e92848e3461 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java
@@ -90,7 +90,7 @@ void methodCorsPreFlight() {
@Test
void methods() {
- RequestPredicate predicate = RequestPredicates.methods(HttpMethod.GET, HttpMethod.HEAD);
+ RequestPredicate predicate = RequestPredicates.methods(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.QUERY);
MockServerHttpRequest mockRequest = MockServerHttpRequest.get("https://example.com").build();
ServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList());
assertThat(predicate.test(request)).isTrue();
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java
index 2861be79a47f..2d5998963aa0 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java
@@ -139,7 +139,7 @@ void options() {
Mono responseMono = this.handlerFunction.handle(request);
Mono result = responseMono.flatMap(response -> {
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
- assertThat(response.headers().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS));
+ assertThat(response.headers().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.QUERY));
return response.writeTo(exchange, context);
});
@@ -148,7 +148,7 @@ void options() {
.expectComplete()
.verify();
assertThat(mockResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
- assertThat(mockResponse.getHeaders().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS));
+ assertThat(mockResponse.getHeaders().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.QUERY));
StepVerifier.create(mockResponse.getBody()).expectComplete().verify();
}
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..52c6d59db207 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
@@ -377,7 +377,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 +395,13 @@ 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);
+ }
}
}
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.java
index 7098d7ffc3bf..a40b42b3e56d 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.java
@@ -117,7 +117,7 @@ void preFlightRequestWithCorsEnabled(HttpServer httpServer) throws Exception {
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getHeaders().getAccessControlAllowOrigin()).isEqualTo("*");
assertThat(entity.getHeaders().getAccessControlAllowMethods())
- .containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST);
+ .containsExactly(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.POST);
}
@ParameterizedHttpServerTest
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java
index d548feb87624..b19224fe756d 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java
@@ -45,7 +45,7 @@
*/
abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
- private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
+ private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
private final HttpStatusCode statusCode;
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java
index a56aedabb0eb..799b2ca54f63 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java
@@ -287,6 +287,19 @@ public static RequestPredicate OPTIONS(String pattern) {
return method(HttpMethod.OPTIONS).and(path(pattern));
}
+ /**
+ * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code QUERY}
+ * and the given {@code pattern} matches against the request path.
+ * @param pattern the path pattern to match against
+ * @return a predicate that matches if the request method is QUERY and if the given pattern
+ * matches against the request path
+ * @since 7.1
+ * @see org.springframework.web.util.pattern.PathPattern
+ */
+ public static RequestPredicate QUERY(String pattern) {
+ return method(HttpMethod.QUERY).and(path(pattern));
+ }
+
/**
* Return a {@code RequestPredicate} that matches if the request's path has the given extension.
* @param extension the path extension to match against, ignoring case
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ResourceHandlerFunction.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ResourceHandlerFunction.java
index 8f2379c30f5a..f4a80a90adb4 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ResourceHandlerFunction.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ResourceHandlerFunction.java
@@ -41,7 +41,7 @@
class ResourceHandlerFunction implements HandlerFunction {
private static final Set SUPPORTED_METHODS =
- Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS);
+ Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.OPTIONS);
private final Resource resource;
@@ -63,6 +63,11 @@ public ServerResponse handle(ServerRequest request) {
.headers(headers -> this.headersConsumer.accept(this.resource, headers))
.build();
}
+ else if (HttpMethod.QUERY.equals(method)) {
+ return EntityResponse.fromObject(this.resource)
+ .headers(headers -> this.headersConsumer.accept(this.resource, headers))
+ .build();
+ }
else if (HttpMethod.HEAD.equals(method)) {
Resource headResource = new HeadMethodResource(this.resource);
return EntityResponse.fromObject(headResource)
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java
index 4505ac7b7ff2..85a58f41bda1 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java
@@ -246,6 +246,30 @@ public RouterFunctions.Builder OPTIONS(
return add(RequestPredicates.OPTIONS(pattern).and(predicate), function);
}
+ // QUERY
+
+ @Override
+ public RouterFunctions.Builder QUERY(HandlerFunction handlerFunction) {
+ return add(RequestPredicates.method(HttpMethod.QUERY), handlerFunction);
+ }
+
+ @Override
+ public RouterFunctions.Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction) {
+ return add(RequestPredicates.method(HttpMethod.QUERY).and(predicate), handlerFunction);
+ }
+
+ @Override
+ public RouterFunctions.Builder QUERY(String pattern, HandlerFunction handlerFunction) {
+ return add(RequestPredicates.QUERY(pattern), handlerFunction);
+ }
+
+ @Override
+ public RouterFunctions.Builder QUERY(String pattern, RequestPredicate predicate,
+ HandlerFunction handlerFunction) {
+
+ return add(RequestPredicates.QUERY(pattern).and(predicate), handlerFunction);
+ }
+
// other
@Override
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java
index d08d69554662..0e5c9aa968f0 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java
@@ -655,6 +655,59 @@ Builder OPTIONS(
Builder OPTIONS(
String pattern, RequestPredicate predicate, HandlerFunction handlerFunction);
+ /**
+ * Adds a route to the given handler function that handles HTTP {@code QUERY} requests.
+ * @param handlerFunction the handler function to handle all {@code QUERY} requests
+ * @return this builder
+ * @since 7.1
+ */
+ Builder QUERY(HandlerFunction handlerFunction);
+
+ /**
+ * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests
+ * that match the given pattern.
+ * @param pattern the pattern to match to
+ * @param handlerFunction the handler function to handle all {@code QUERY} requests that
+ * match {@code pattern}
+ * @return this builder
+ * @since 7.1
+ * @see org.springframework.web.util.pattern.PathPattern
+ */
+ Builder QUERY(String pattern, HandlerFunction handlerFunction);
+
+ /**
+ * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests
+ * that match the given predicate.
+ * @param predicate predicate to match
+ * @param handlerFunction the handler function to handle all {@code QUERY} requests that
+ * match {@code predicate}
+ * @return this builder
+ * @since 7.1
+ * @see RequestPredicates
+ */
+ Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction);
+
+ /**
+ * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests
+ * that match the given pattern and predicate.
+ *
For instance, the following example routes QUERY requests for "/user" that contain JSON
+ * to the {@code addUser} method in {@code userController}:
+ *