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 e9d4a5fa3239..b7c9708c652e 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 f8c896493a0f..eb4c64a3a097 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 @@ -144,6 +144,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 6d3a91a4f6c5..5a157ed68e90 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/client/DefaultRestTestClient.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java index fe01246f775e..480383595007 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 @@ -112,6 +112,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 7b7385b4ae63..f16fd65fb5e6 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 @@ -120,6 +120,12 @@ public interface RestTestClient { */ RequestHeadersUriSpec delete(); + /** + * Prepare an HTTP QUERY request. + * @return a spec for specifying the target URL + */ + 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 e2e1dcb5ca52..4b446e25eb5a 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/client/RestTestClientTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/client/RestTestClientTests.java index ee1393c16b15..4754aed3566c 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 @@ -63,7 +63,7 @@ void setUp() { class HttpMethods { @ParameterizedTest - @ValueSource(strings = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"}) + @ValueSource(strings = {"GET", "POST", "PUT", "DELETE", "PATCH", "QUERY", "HEAD"}) void testMethod(String method) { RestTestClientTests.this.client.method(HttpMethod.valueOf(method)).uri("/test") .exchange() @@ -124,10 +124,18 @@ void testOptions() { 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 60f25994c3bb..0891a54eb89b 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,8 @@ 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; import org.springframework.http.MediaType; @@ -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 581729453daf..857edbe79485 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 608d7d535a1c..52ff8bec6fbf 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 6812f3eb936c..009f53a47aa0 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 0f5b8c4bf406..bca555758ccd 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 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..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 07f994561780..43a23b25f0ca 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) @@ -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 bdc292be0abe..5290081749c0 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 61652c6cf292..cf5a614081d7 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,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 a6bbd0b68954..4bf431f10a60 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 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/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 0e3eb72f4fdd..1b022c769eb6 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..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 fe2084380d0b..595bc7c43588 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 1fb6205aad70..ec0f3d8b9b50 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..1d5e78f10c6d 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 5d33f8266b7d..8ee640525348 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 1a0a63fb14c1..745e6c5598b0 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 0e1ae35ce5b3..075ad0c9b4bb 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 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 0de9d60a017d..047a309d3ff4 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 6dfca7148d6d..782cbd4c295e 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 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 b09cb4628883..7e2ee721de0c 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 20d073a73866..39e724db7440 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 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 2fd02b0a35ae..dba25470e48b 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 @@ -287,6 +287,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 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 271e8f00681d..d5b15f584ae6 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 834aeef8b5b0..e6b5a1a41dc3 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 @@ -692,6 +692,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}: + *

    +		 * RouterFunction<ServerResponse> route =
    +		 *   RouterFunctions.route()
    +		 *     .QUERY("/user", RequestPredicates.contentType(MediaType.APPLICATION_JSON), userController::addUser)
    +		 *     .build();
    +		 * 
    + * @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 3704dcb8d092..bcd77dbe636c 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 0fafb07c6089..94b09f8d9c03 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 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/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/ResourceHandlerFunctionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java index 5bc98628961d..5017d457658e 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 bd8c0e13e68f..2d68fe6f8839 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 f8ff99ddda39..7d69db73c2a3 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 736bbea91fe2..342f7e68f929 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 3cf40bd24ae5..97f12ac92d25 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 @@ -286,6 +286,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 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 9aaf05b55846..ce7b074e6b08 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 9c6739e91e4a..fbba91b7a4be 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}: + *

    +		 * RouterFunction<ServerResponse> route =
    +		 *   RouterFunctions.route()
    +		 *     .QUERY("/user", RequestPredicates.contentType(MediaType.APPLICATION_JSON), userController::addUser)
    +		 *     .build();
    +		 * 
    + * @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-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java index 5e6e080be7c3..5ff8261be792 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java @@ -162,6 +162,9 @@ else if (isEmpty()) { if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) { return requestMethodConditionCache.get(HttpMethod.GET.name()); } + if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.QUERY)) { + return requestMethodConditionCache.get(HttpMethod.QUERY.name()); + } } return null; } @@ -189,6 +192,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-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java index 0856a0db021d..b4022d6e741e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java @@ -519,7 +519,7 @@ private static Set initAllowedHttpMethods(Set declaredMethod for (String method : declaredMethods) { HttpMethod httpMethod = HttpMethod.valueOf(method); result.add(httpMethod); - if (httpMethod == HttpMethod.GET) { + if (httpMethod == HttpMethod.GET || httpMethod == HttpMethod.QUERY) { result.add(HttpMethod.HEAD); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index ed86ade4da6e..ce9b88769a01 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -243,7 +243,7 @@ else if (returnValue instanceof ProblemDetail detail) { outputMessage.getServletResponse().setStatus(returnStatus.value()); if (returnStatus.value() == HttpStatus.OK.value()) { HttpMethod method = inputMessage.getMethod(); - if ((HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method)) && + if ((HttpMethod.GET.equals(method) || HttpMethod.QUERY.equals(method) || HttpMethod.HEAD.equals(method)) && isResourceNotModified(inputMessage, outputMessage)) { outputMessage.flush(); return; @@ -292,7 +292,7 @@ private boolean isResourceNotModified(ServletServerHttpRequest request, ServletS HttpHeaders responseHeaders = response.getHeaders(); String etag = responseHeaders.getETag(); long lastModifiedTimestamp = responseHeaders.getLastModified(); - if (request.getMethod() == HttpMethod.GET || request.getMethod() == HttpMethod.HEAD) { + if (request.getMethod() == HttpMethod.GET || request.getMethod() == HttpMethod.QUERY || request.getMethod() == HttpMethod.HEAD) { responseHeaders.remove(HttpHeaders.ETAG); responseHeaders.remove(HttpHeaders.LAST_MODIFIED); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index 8949b91d4134..e4c98515cd8e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -138,7 +138,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator public ResourceHttpRequestHandler() { - super(HttpMethod.GET.name(), HttpMethod.HEAD.name()); + super(HttpMethod.GET.name(), HttpMethod.QUERY.name(), HttpMethod.HEAD.name()); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index 6f960f64a1db..33d07672f4ef 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -931,7 +931,7 @@ void testCorsMinimal() { CorsConfiguration config = configs.get("/**"); assertThat(config).isNotNull(); assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[]{"*"}); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[]{"GET", "HEAD", "POST"}); + assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[]{"GET", "QUERY", "HEAD", "POST"}); assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[]{"*"}); assertThat(config.getExposedHeaders()).isNull(); assertThat(config.getAllowCredentials()).isNull(); @@ -964,7 +964,7 @@ void testCors() { assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123)); config = configs.get("/resources/**"); assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[]{"https://domain1.com"}); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[]{"GET", "HEAD", "POST"}); + assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[]{"GET", "QUERY", "HEAD", "POST"}); assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[]{"*"}); assertThat(config.getExposedHeaders()).isNull(); assertThat(config.getAllowCredentials()).isNull(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java index 97059add9400..da17ebb56904 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java @@ -175,7 +175,7 @@ void options() throws ServletException, IOException { ServerResponse response = this.handlerFunction.handle(request); 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.QUERY, HttpMethod.HEAD, HttpMethod.OPTIONS)); MockHttpServletResponse servletResponse = new MockHttpServletResponse(); ModelAndView mav = response.writeTo(servletRequest, servletResponse, this.context); @@ -184,7 +184,7 @@ void options() throws ServletException, IOException { assertThat(servletResponse.getStatus()).isEqualTo(200); String allowHeader = servletResponse.getHeader("Allow"); String[] methods = StringUtils.tokenizeToStringArray(allowHeader, ","); - assertThat(methods).containsExactlyInAnyOrder("GET","HEAD","OPTIONS"); + assertThat(methods).containsExactlyInAnyOrder("GET","QUERY","HEAD","OPTIONS"); byte[] actualBytes = servletResponse.getContentAsByteArray(); assertThat(actualBytes).isEmpty(); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java index fc3f5c60db52..bc08240a521e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java @@ -185,7 +185,7 @@ void abortInterceptorInPreFlightRequestWithCorsConfig() throws Exception { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain.com"); - assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,HEAD"); + assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,QUERY,HEAD"); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java index 8ca35f451ff2..9de272cf1b59 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java @@ -193,9 +193,10 @@ void getHandlerMediaTypeNotSupportedWithParseError(TestRequestMappingInfoHandler void getHandlerHttpOptions(TestRequestMappingInfoHandlerMapping mapping) throws Exception { testHttpOptions(mapping, "/foo", "GET,HEAD,OPTIONS", null); testHttpOptions(mapping, "/person/1", "PUT,OPTIONS", null); - testHttpOptions(mapping, "/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS", null); + testHttpOptions(mapping, "/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,QUERY", null); testHttpOptions(mapping, "/something", "PUT,POST", null); testHttpOptions(mapping, "/qux", "PATCH,GET,HEAD,OPTIONS", new MediaType("foo", "bar")); + testHttpOptions(mapping, "/quid", "QUERY,HEAD,OPTIONS", null); } @PathPatternsParameterizedTest @@ -572,6 +573,11 @@ public String getBaz() { @RequestMapping(value = "/qux", method = RequestMethod.PATCH, consumes = "foo/bar") public void patchBaz(String value) { } + + @RequestMapping(value = "/quid", method = RequestMethod.QUERY, consumes = "application/json", produces = "application/json") + public String query(@RequestBody String body) { + return "{}"; + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java index 46bc91fd61c2..687c299ae8f2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -149,6 +149,23 @@ void patchHttpMediaTypeNotSupported() { assertThat(headers.getFirst(HttpHeaders.ACCEPT_PATCH)).isEqualTo("application/atom+xml, application/xml"); } + @Test + void queryHttpMediaTypeNotSupported() { + this.servletRequest = new MockHttpServletRequest("QUERY", "/"); + this.request = new ServletWebRequest(this.servletRequest, this.servletResponse); + + ResponseEntity entity = testException( + new HttpMediaTypeNotSupportedException( + MediaType.APPLICATION_JSON, + List.of(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML), + HttpMethod.QUERY)); + + HttpHeaders headers = entity.getHeaders(); + assertThat(headers.getFirst(HttpHeaders.ACCEPT)).isEqualTo("application/atom+xml, application/xml"); + assertThat(headers.getFirst(HttpHeaders.ACCEPT)).isEqualTo("application/atom+xml, application/xml"); + assertThat(headers.getFirst(HttpHeaders.ACCEPT_QUERY)).isEqualTo("application/atom+xml, application/xml"); + } + @Test void httpMediaTypeNotAcceptable() { testException(new HttpMediaTypeNotAcceptableException("")); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java index eeae66ae5f5f..4b405094d252 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java @@ -116,7 +116,7 @@ void supportsOptionsRequests() throws Exception { this.handler.handleRequest(this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(200); - assertThat(this.response.getHeader("Allow")).isEqualTo("GET,HEAD,OPTIONS"); + assertThat(this.response.getHeader("Allow")).isEqualTo("GET,QUERY,HEAD,OPTIONS"); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/support/WebContentGeneratorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/support/WebContentGeneratorTests.java index 4970cf998f58..c785f19223d8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/support/WebContentGeneratorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/support/WebContentGeneratorTests.java @@ -39,7 +39,7 @@ void getAllowHeaderWithConstructorTrue() { @Test void getAllowHeaderWithConstructorFalse() { WebContentGenerator generator = new TestWebContentGenerator(false); - assertThat(generator.getAllowHeader()).isEqualTo("GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"); + assertThat(generator.getAllowHeader()).isEqualTo("GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,QUERY"); } @Test @@ -59,7 +59,7 @@ void getAllowHeaderWithSupportedMethodsSetter() { void getAllowHeaderWithSupportedMethodsSetterEmpty() { WebContentGenerator generator = new TestWebContentGenerator(); generator.setSupportedMethods(); - assertThat(generator.getAllowHeader()).as("Effectively \"no restriction\" on supported methods").isEqualTo("GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"); + assertThat(generator.getAllowHeader()).as("Effectively \"no restriction\" on supported methods").isEqualTo("GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,QUERY"); } @Test