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 d28816d52b30..06c28a0b5abf 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 POST 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 60b39d57e76b..84d05bd4b7be 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
@@ -165,6 +165,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 b7018bcdda2f..c47a046fa1ff 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,12 @@ public interface WebTestClient {
*/
RequestHeadersUriSpec> options();
+ /**
+ * Prepare an HTTP QUERY request.
+ * @return a spec for specifying the target URL
+ */
+ 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 9c8cfcc50d03..af3651fee683 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,20 @@ 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
+ */
+ 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/request/MockMvcRequestBuilders.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java
index abf479391090..f7560f302e79 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,24 @@ 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
+ */
+ 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 x.x.x
+ */
+ 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/request/MockHttpServletRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java
index 5c09d17fa085..4fc0696ad62d 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
@@ -32,6 +32,7 @@
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
@@ -53,6 +54,7 @@
import static org.assertj.core.api.Assertions.entry;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
+import static org.springframework.http.HttpMethod.QUERY;
/**
* Tests for building a {@link MockHttpServletRequest} with
@@ -391,17 +393,20 @@ void requestParameterFromMultiValueMap() {
}
@Test
+ @ParameterizedTest()
void requestParameterFromRequestBodyFormData() {
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")
- .contentType(contentType).content(body.getBytes(UTF_8))
- .buildRequest(this.servletContext);
+ for (HttpMethod method : List.of(POST, QUERY)) {
+ 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);
+ 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 ee4d794e0e4c..499d757093f5 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
@@ -130,6 +130,12 @@ 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.
+ * @see IETF Draft
+ */
+ public static final String ACCEPT_QUERY = "Accept-Query";
/**
* The CORS {@code Access-Control-Allow-Credentials} response header field name.
* @see CORS W3C recommendation
@@ -635,6 +641,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 x.x.x
+ */
+ 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 x.x.x
+ */
+ 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 95debcfbcaa2..390188e0b4fc 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java
@@ -37,25 +37,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");
@@ -67,23 +67,29 @@ 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}.
+ * @see IETF Draft
+ */
+ 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;
@@ -97,7 +103,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.
@@ -124,6 +130,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 64fc4316f05d..f8847fe814f7 100644
--- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java
+++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java
@@ -382,6 +382,26 @@ 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
+ */
+ 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 x.x.x
+ */
+ 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 d366eded4562..c82d608349b4 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;
@@ -345,6 +346,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 bcba1c9953c1..3441e371e7e3 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 ef09e5124056..4a71e0f8ae1d 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 5c1e962ac419..4135f865c84f 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 1cce89e2545b..60902058c609 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 8cfb36a6e389..943bac175015 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 8cfc46101a93..86f551eb783f 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..42af68d8a0d6
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2024 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 x.x.x
+ * @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 6cb0386e8482..663f51f5537d 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}.
*
*
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)
@@ -121,7 +122,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 b1f5438b9048..e1ca546ead43 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 d39e43bd5f20..4db69a3f9b1a 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
@@ -193,6 +193,11 @@ public RequestHeadersUriSpec> options() {
return methodInternal(HttpMethod.OPTIONS);
}
+ @Override
+ public RequestHeadersUriSpec> 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 9804bf55bab8..4b8b5ce2d9f0 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
@@ -123,6 +123,12 @@ public interface RestClient {
*/
RequestHeadersUriSpec> options();
+ /**
+ * Start building an HTTP QUERY request.
+ * @return a spec for specifying the target URL
+ */
+ RequestHeadersUriSpec> 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/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java
index 6873edf5d665..c8e6049f9d4f 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/ShallowEtagHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java
index 3564cc8629ca..0b826637e71c 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/server/UnsupportedMediaTypeStatusException.java b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java
index f38dd40b9501..14ca50d4b38e 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 a3ec76997b46..6a61fcfd451a 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 745e59533d5b..2b12bed9eaa6 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..21e04047c4ce
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/service/annotation/QueryExchange.java
@@ -0,0 +1,53 @@
+package org.springframework.web.service.annotation;
+
+import org.springframework.core.annotation.AliasFor;
+
+import java.lang.annotation.*;
+
+/**
+ * Shortcut for {@link HttpExchange @HttpExchange} for HTTP QUERY requests.
+ *
+ * @author Mario Ruiz
+ * @since x.x.x
+ */
+@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 61ebf19faa1a..6b291f1606de 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
@@ -18,9 +18,11 @@ 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
+import org.springframework.web.client.queryForEntity
import java.lang.Class
import java.net.URI
import kotlin.reflect.KClass
@@ -291,3 +293,45 @@ 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.postForEntity] providing a `postForEntity(...)`
+ * 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 x.x.x
+ */
+@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.postForEntity] providing a `postForEntity(...)`
+ * 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 x.x.x
+ */
+@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.postForEntity] providing a `postForEntity(...)`
+ * 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 x.x.x
+ */
+@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 055149ea53e1..eb5aff2f6975 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 b46afe89e954..f8969a3979b0 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")
+ HttpEntity queryRawHttpEntity(HttpEntity entity) {
+ return new HttpEntity(new Response("response"));
+ }
+
}
@RestController
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 18b8815e9595..05384e732423 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
@@ -74,7 +74,7 @@ void tearDown() throws Exception {
private MockResponse getRequest(RecordedRequest request, byte[] body, String contentType) {
if (request.getMethod().equals("OPTIONS")) {
- return new MockResponse().setResponseCode(200).setHeader("Allow", "GET, OPTIONS, HEAD, TRACE");
+ return new MockResponse().setResponseCode(200).setHeader("Allow", "GET, QUERY, OPTIONS, HEAD, TRACE");
}
Buffer buf = new Buffer();
buf.write(body);
@@ -231,6 +231,28 @@ private MockResponse putRequest(RecordedRequest request, String expectedRequestC
return new MockResponse().setResponseCode(202);
}
+ private MockResponse queryRequest(RecordedRequest request, String expectedRequestContent,
+ String contentType, byte[] responseBody) {
+
+ assertThat(request.getHeaders().values(CONTENT_LENGTH)).hasSize(1);
+ assertThat(Integer.parseInt(request.getHeader(CONTENT_LENGTH))).as("Invalid request content-length").isGreaterThan(0);
+ String requestContentType = request.getHeader(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().readString(charset)).as("Invalid request body").isEqualTo(expectedRequestContent);
+ Buffer buf = new Buffer();
+ buf.write(responseBody);
+ return new MockResponse()
+ .setHeader(CONTENT_TYPE, contentType)
+ .setHeader(CONTENT_LENGTH, responseBody.length)
+ .setBody(buf)
+ .setResponseCode(200);
+ }
+
protected class TestDispatcher extends Dispatcher {
@@ -293,6 +315,9 @@ else if (request.getPath().equals("/patch")) {
else if (request.getPath().equals("/put")) {
return putRequest(request, helloWorld);
}
+ else if (request.getPath().equals("/query")) {
+ return queryRequest(request, helloWorld, textContentType.toString(), helloWorldBytes);
+ }
return new MockResponse().setResponseCode(404);
}
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 022309b30813..b8910210dc4d 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
@@ -290,7 +290,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 4c421a56bded..13b047918e94 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
@@ -43,6 +43,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
+import org.springframework.http.RequestEntity;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInitializer;
@@ -75,6 +76,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;
/**
@@ -478,6 +480,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/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java
index 092781475f94..1f8fb86ea426 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 bcb4ae852365..b5add6057149 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 988175990506..38253acdd0db 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/testFixtures/java/org/springframework/web/testfixture/method/MvcAnnotationPredicates.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/MvcAnnotationPredicates.java
index 9bde5d3eab40..81bc715fcb48 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 f16d0c1a4637..49f025250942 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 RequestHeadersUriSpec> 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 1b07d1016517..f254a1864e2f 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,12 @@ public interface WebClient {
*/
RequestHeadersUriSpec> options();
+ /**
+ * Start building an HTTP QUERY request.
+ * @return a spec for specifying the target URL
+ */
+ RequestHeadersUriSpec> 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 090f0450b02e..67151f2c001d 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 6ee7be2c2c21..d3697d997999 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
@@ -266,6 +266,18 @@ 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
+ * @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 56ea5b1fb735..c82c9c13470f 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 e8133b2f5cc0..76bd64dcb18d 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
@@ -231,6 +231,30 @@ public RouterFunctions.Builder OPTIONS(String pattern, RequestPredicate predicat
return add(RequestPredicates.OPTIONS(pattern).and(predicate), handlerFunction);
}
+ // 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 8331ceb0f5aa..ad1397f01cd7 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
@@ -696,6 +696,58 @@ public interface Builder {
*/
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 x.x.x
+ */
+ 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
+ * @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 x.x.x
+ * @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
+ * @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 d108080a1082..487998146350 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/condition/RequestMethodsRequestCondition.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java
index 9289aee8fea0..4257c366f246 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java
@@ -157,6 +157,9 @@ else if (isEmpty()) {
if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) {
return requestMethodConditionCache.get(HttpMethod.GET);
}
+ if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.QUERY)) {
+ return requestMethodConditionCache.get(HttpMethod.QUERY);
+ }
}
return null;
}
@@ -184,6 +187,9 @@ else if (this.methods.size() == 1) {
else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) {
return 1;
}
+ else if (this.methods.contains(RequestMethod.QUERY) && other.methods.contains(RequestMethod.HEAD)) {
+ return 1;
+ }
}
return 0;
}
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 73e8146714fe..d1b345fa0a62 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/ResponseEntityResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java
index 1961429a1d66..077db094b444 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/ResourceHandlerFunctionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java
index 8d270c98a378..141f60d0a982 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
@@ -141,7 +141,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);
});
@@ -150,7 +150,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 bbb0b7c7643d..85e2e4f2a946 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,9 +395,15 @@ 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 testMediaTypeNotAcceptable(String url) {
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 002feb2aa1af..ce4a78c6cf01 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 d2288fc3f3d2..db6b8aa34f12 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
@@ -43,7 +43,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 6333b87c7870..4bf0fd99e98b 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
@@ -265,6 +265,18 @@ 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
+ * @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 86ccce7dec01..c16d715ed714 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 6a5c4806b731..7ce41621ec81 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
@@ -229,6 +229,30 @@ public RouterFunctions.Builder OPTIONS(String pattern, RequestPredicate predicat
return add(RequestPredicates.OPTIONS(pattern).and(predicate), handlerFunction);
}
+ // 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 f2ea64ea97ab..048670178520 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
@@ -610,6 +610,57 @@ public interface Builder {
*/
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 x.x.x
+ */
+ 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
+ * @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 x.x.x
+ * @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}:
+ *