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 6b0dd46fcdb5..0fd4ba20358b 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 @@ -53,6 +53,7 @@ import static org.assertj.core.api.Assertions.entry; import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.HttpMethod.QUERY; /** * Tests for building a {@link MockHttpServletRequest} with @@ -395,13 +396,15 @@ void requestParameterFromRequestBodyFormData() { String contentType = "application/x-www-form-urlencoded;charset=UTF-8"; String body = "name+1=value+1&name+2=value+A&name+2=value+B&name+3"; - MockHttpServletRequest request = new MockHttpServletRequestBuilder(POST).uri("/foo") - .contentType(contentType).content(body.getBytes(UTF_8)) - .buildRequest(this.servletContext); + for (HttpMethod method : List.of(POST, QUERY)) { + MockHttpServletRequest request = new MockHttpServletRequestBuilder(method).uri("/foo") + .contentType(contentType).content(body.getBytes(UTF_8)) + .buildRequest(this.servletContext); - assertThat(request.getParameterMap().get("name 1")).containsExactly("value 1"); - assertThat(request.getParameterMap().get("name 2")).containsExactly("value A", "value B"); - assertThat(request.getParameterMap().get("name 3")).containsExactly((String) null); + assertThat(request.getParameterMap().get("name 1")).containsExactly("value 1"); + assertThat(request.getParameterMap().get("name 2")).containsExactly("value A", "value B"); + assertThat(request.getParameterMap().get("name 3")).containsExactly((String) null); + } } @Test diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 5d895e4a368d..d379bac15b8e 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -132,6 +132,12 @@ public class HttpHeaders implements MultiValueMap, 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 @@ -595,6 +601,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 e10185ecb462..24848e47b2d9 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -36,25 +36,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"); @@ -66,23 +66,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; @@ -96,7 +102,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. @@ -123,6 +129,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 d387735f8b7a..496d6480c565 100644 --- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java +++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java @@ -335,6 +335,26 @@ public static BodyBuilder post(String uriTemplate, Object... uriVariables) { 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 6d62a8ffbb34..492b407e23fa 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; @@ -347,6 +348,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 4ce57138a5d3..3260978c341f 100644 --- a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java +++ b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java @@ -134,6 +134,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 4681a719a9ba..fbdea9bf4f4f 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 22092f77fc51..43e3fbb0bf0a 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 c11f39e4b0a7..140156fed309 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 18a0b47db553..75df360a9c4c 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 8e8cb005d0a4..d5c5f89355c5 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..66eb8ce2ec2d --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java @@ -0,0 +1,98 @@ +/* + * 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 {}; + +} 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 d01491b34697..5b9ca01079f3 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 @@ -121,7 +121,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 c7564093cdd9..eb1bc3816ef1 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 @@ -25,7 +25,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. @@ -38,7 +38,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 static RequestMethod resolve(String method) { case "DELETE" -> DELETE; case "OPTIONS" -> OPTIONS; case "TRACE" -> TRACE; + case "QUERY" -> QUERY; default -> null; }; } @@ -93,6 +94,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 9e88dfdc91a0..9496f9389574 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 @@ -185,6 +185,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 1b7016d77e5a..13ec2c57ce4a 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 @@ -122,6 +122,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 bef29cf0e2b4..d1e8a50f11ba 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 @@ -62,9 +62,9 @@ public class CorsConfiguration { private static final List DEFAULT_PERMIT_ALL = Collections.singletonList(ALL); - private static final List DEFAULT_METHODS = List.of(HttpMethod.GET, HttpMethod.HEAD); + private static final List DEFAULT_METHODS = List.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); - private static final List DEFAULT_PERMIT_METHODS = List.of(HttpMethod.GET.name(), + private static final List DEFAULT_PERMIT_METHODS = List.of(HttpMethod.GET.name(), HttpMethod.QUERY.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); diff --git a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java index 3564cc8629ca..0b826637e71c 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java @@ -151,7 +151,7 @@ protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletRespo if (!response.isCommitted() && responseStatusCode >= 200 && responseStatusCode < 300 && - HttpMethod.GET.matches(request.getMethod())) { + (HttpMethod.GET.matches(request.getMethod()) || HttpMethod.QUERY.matches(request.getMethod()))) { String cacheControl = response.getHeader(HttpHeaders.CACHE_CONTROL); return (cacheControl == null || !cacheControl.contains(DIRECTIVE_NO_STORE)); diff --git a/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java index a9bed587b965..c81cee7df084 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 @@ -165,6 +165,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 0500180897b1..f07b8b18a593 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/test/java/org/springframework/http/HttpMethodTests.java b/spring-web/src/test/java/org/springframework/http/HttpMethodTests.java index 055149ea53e1..eb5aff2f6975 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpMethodTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpMethodTests.java @@ -44,12 +44,12 @@ void comparison() { void values() { HttpMethod[] values = HttpMethod.values(); assertThat(values).containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST, HttpMethod.PUT, - HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE); + HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE, HttpMethod.QUERY); // check defensive copy values[0] = HttpMethod.POST; assertThat(HttpMethod.values()).containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST, HttpMethod.PUT, - HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE); + HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE, HttpMethod.QUERY); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java index 18b8815e9595..05384e732423 100644 --- a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java @@ -74,7 +74,7 @@ void tearDown() throws Exception { private MockResponse getRequest(RecordedRequest request, byte[] body, String contentType) { if (request.getMethod().equals("OPTIONS")) { - return new MockResponse().setResponseCode(200).setHeader("Allow", "GET, OPTIONS, HEAD, TRACE"); + return new MockResponse().setResponseCode(200).setHeader("Allow", "GET, QUERY, OPTIONS, HEAD, TRACE"); } Buffer buf = new Buffer(); buf.write(body); @@ -231,6 +231,28 @@ private MockResponse putRequest(RecordedRequest request, String expectedRequestC return new MockResponse().setResponseCode(202); } + private MockResponse queryRequest(RecordedRequest request, String expectedRequestContent, + String contentType, byte[] responseBody) { + + assertThat(request.getHeaders().values(CONTENT_LENGTH)).hasSize(1); + assertThat(Integer.parseInt(request.getHeader(CONTENT_LENGTH))).as("Invalid request content-length").isGreaterThan(0); + String requestContentType = request.getHeader(CONTENT_TYPE); + assertThat(requestContentType).as("No content-type").isNotNull(); + Charset charset = StandardCharsets.ISO_8859_1; + if (requestContentType.contains("charset=")) { + String charsetName = requestContentType.split("charset=")[1]; + charset = Charset.forName(charsetName); + } + assertThat(request.getBody().readString(charset)).as("Invalid request body").isEqualTo(expectedRequestContent); + Buffer buf = new Buffer(); + buf.write(responseBody); + return new MockResponse() + .setHeader(CONTENT_TYPE, contentType) + .setHeader(CONTENT_LENGTH, responseBody.length) + .setBody(buf) + .setResponseCode(200); + } + protected class TestDispatcher extends Dispatcher { @@ -293,6 +315,9 @@ else if (request.getPath().equals("/patch")) { else if (request.getPath().equals("/put")) { return putRequest(request, helloWorld); } + else if (request.getPath().equals("/query")) { + return queryRequest(request, helloWorld, textContentType.toString(), helloWorldBytes); + } return new MockResponse().setResponseCode(404); } catch (Throwable ex) { diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index cf7f6fadd44e..2970f8ce0823 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; @@ -72,6 +73,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; /** @@ -473,6 +475,46 @@ void postForEntityNull() throws Exception { verify(response).close(); } + @Test + void queryForEntity() throws Exception { + mockTextPlainHttpMessageConverter(); + HttpHeaders requestHeaders = new HttpHeaders(); + mockSentRequest(QUERY, "https://example.com", requestHeaders); + mockResponseStatus(HttpStatus.OK); + String expected = "42"; + mockResponseBody(expected, MediaType.TEXT_PLAIN); + + ResponseEntity result = template.exchange(RequestEntity.query("https://example.com").body("Hello World"), String.class); + assertThat(result.getBody()).as("Invalid QUERY result").isEqualTo(expected); + assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN); + assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE); + assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK); + + verify(response).close(); + } + + @Test + void queryForEntityNull() throws Exception { + mockTextPlainHttpMessageConverter(); + HttpHeaders requestHeaders = new HttpHeaders(); + mockSentRequest(QUERY, "https://example.com", requestHeaders); + mockResponseStatus(HttpStatus.OK); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(MediaType.TEXT_PLAIN); + responseHeaders.setContentLength(10); + given(response.getHeaders()).willReturn(responseHeaders); + given(response.getBody()).willReturn(InputStream.nullInputStream()); + given(converter.read(String.class, response)).willReturn(null); + + ResponseEntity result = template.exchange("https://example.com",QUERY, null, String.class); + assertThat(result.hasBody()).as("Invalid QUERY result").isFalse(); + assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN); + assertThat(requestHeaders.getContentLength()).as("Invalid content length").isEqualTo(0); + assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK); + + verify(response).close(); + } + @Test void put() throws Exception { mockTextPlainHttpMessageConverter(); diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 092781475f94..1f8fb86ea426 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -140,7 +140,7 @@ void combineWithDefaultPermitValues() { assertThat(config.getAllowedHeaders()).containsExactly("*"); assertThat(combinedConfig).isNotNull(); assertThat(combinedConfig.getAllowedMethods()) - .containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); + .containsExactly(HttpMethod.GET.name(), HttpMethod.QUERY.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); assertThat(combinedConfig.getExposedHeaders()).isEmpty(); combinedConfig = new CorsConfiguration().combine(config); @@ -148,7 +148,7 @@ void combineWithDefaultPermitValues() { assertThat(config.getAllowedHeaders()).containsExactly("*"); assertThat(combinedConfig).isNotNull(); assertThat(combinedConfig.getAllowedMethods()) - .containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); + .containsExactly(HttpMethod.GET.name(), HttpMethod.QUERY.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); assertThat(combinedConfig.getExposedHeaders()).isEmpty(); } @@ -394,7 +394,9 @@ void checkOriginPatternNotAllowed() { @Test void checkMethodAllowed() { CorsConfiguration config = new CorsConfiguration(); - assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET, HttpMethod.HEAD); + assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); + assertThat(config.checkHttpMethod(HttpMethod.QUERY)).containsExactly(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); + config.addAllowedMethod("GET"); assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET); @@ -450,7 +452,7 @@ void changePermitDefaultValues() { assertThat(config.getAllowedOrigins()).containsExactly("*", "https://domain.com"); assertThat(config.getAllowedHeaders()).containsExactly("*", "header1"); - assertThat(config.getAllowedMethods()).containsExactly("GET", "HEAD", "POST", "PATCH"); + assertThat(config.getAllowedMethods()).containsExactly("GET", "QUERY", "HEAD", "POST", "PATCH"); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index ad5964f5374b..07ae6016c382 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 df86b40e8bff..ea7d3652958f 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 @@ -262,7 +262,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-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 443ba3018f9a..84367e25365b 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 @@ -171,6 +171,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 69e07aebf589..817863d71773 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 @@ -121,6 +121,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 7467d2b8fef7..335834eddc60 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 @@ -293,7 +293,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 157a360aa7a8..25ea9b46674a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java @@ -266,6 +266,18 @@ public static RequestPredicate OPTIONS(String pattern) { return method(HttpMethod.OPTIONS).and(path(pattern)); } + /** + * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code QUERY} + * and the given {@code pattern} matches against the request path. + * @param pattern the path pattern to match against + * @return a predicate that matches if the request method is QUERY and if the given pattern + * matches against the request path + * @see org.springframework.web.util.pattern.PathPattern + */ + public static RequestPredicate QUERY(String pattern) { + return method(HttpMethod.QUERY).and(path(pattern)); + } + /** * Return a {@code RequestPredicate} that matches if the request's path has the given extension. * @param extension the path extension to match against, ignoring case diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java index 2fa724917de2..ab1d3642c3c7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java @@ -43,7 +43,7 @@ class ResourceHandlerFunction implements HandlerFunction { private static final Set SUPPORTED_METHODS = - Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS); + Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.OPTIONS); private final Resource resource; @@ -66,6 +66,12 @@ public Mono handle(ServerRequest request) { .build() .map(response -> response); } + else if (HttpMethod.QUERY.equals(method)) { + return EntityResponse.fromObject(this.resource) + .headers(headers -> this.headersConsumer.accept(this.resource, headers)) + .build() + .map(response -> response); + } else if (HttpMethod.HEAD.equals(method)) { Resource headResource = new HeadMethodResource(this.resource); return EntityResponse.fromObject(headResource) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java index e8133b2f5cc0..76bd64dcb18d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java @@ -231,6 +231,30 @@ public RouterFunctions.Builder OPTIONS(String pattern, RequestPredicate predicat return add(RequestPredicates.OPTIONS(pattern).and(predicate), handlerFunction); } + // QUERY + + @Override + public RouterFunctions.Builder QUERY(HandlerFunction handlerFunction) { + return add(RequestPredicates.method(HttpMethod.QUERY), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction) { + return add(RequestPredicates.method(HttpMethod.QUERY).and(predicate), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(String pattern, HandlerFunction handlerFunction) { + return add(RequestPredicates.QUERY(pattern), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(String pattern, RequestPredicate predicate, + HandlerFunction handlerFunction) { + + return add(RequestPredicates.QUERY(pattern).and(predicate), handlerFunction); + } + // other @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java index d2b81a2278e5..8ef23a80a61d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java @@ -696,6 +696,58 @@ public interface Builder { */ Builder OPTIONS(String pattern, RequestPredicate predicate, HandlerFunction handlerFunction); + /** + * Adds a route to the given handler function that handles HTTP {@code QUERY} requests. + * @param handlerFunction the handler function to handle all {@code QUERY} requests + * @return this builder + * @since x.x.x + */ + Builder QUERY(HandlerFunction handlerFunction); + + /** + * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests + * that match the given pattern. + * @param pattern the pattern to match to + * @param handlerFunction the handler function to handle all {@code QUERY} requests that + * match {@code pattern} + * @return this builder + * @see org.springframework.web.util.pattern.PathPattern + */ + Builder QUERY(String pattern, HandlerFunction handlerFunction); + + /** + * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests + * that match the given predicate. + * @param predicate predicate to match + * @param handlerFunction the handler function to handle all {@code QUERY} requests that + * match {@code predicate} + * @return this builder + * @since x.x.x + * @see RequestPredicates + */ + Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction); + + /** + * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests + * that match the given pattern and predicate. + *

For instance, the following example routes QUERY requests for "/user" that contain JSON + * to the {@code addUser} method in {@code userController}: + *

+		 * 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 cd624891b5c7..21ccf0d907a7 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 e8f0cd32d00d..0ed4d08805bd 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 @@ -159,6 +159,9 @@ private RequestMethodsRequestCondition matchRequestMethod(HttpMethod httpMethod) 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; } @@ -186,6 +189,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 94d0cf9cfc37..8268c17ebe2e 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 @@ -192,8 +192,9 @@ protected HandlerMethod handleNoMatch(Set infos, 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); @@ -326,14 +327,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)); } @@ -403,9 +413,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) { @@ -416,7 +427,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 3d91c62a7de7..c2b513b0d42b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -56,7 +56,7 @@ */ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHandler implements HandlerResultHandler { - private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD); + private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); /** diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java index 8d270c98a378..141f60d0a982 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java @@ -141,7 +141,7 @@ void options() { Mono responseMono = this.handlerFunction.handle(request); Mono result = responseMono.flatMap(response -> { assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.headers().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS)); + assertThat(response.headers().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.QUERY)); return response.writeTo(exchange, context); }); @@ -150,7 +150,7 @@ void options() { .expectComplete() .verify(); assertThat(mockResponse.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(mockResponse.getHeaders().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS)); + assertThat(mockResponse.getHeaders().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.QUERY)); StepVerifier.create(mockResponse.getBody()).expectComplete().verify(); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index 0ba5b8ef598f..a76b6c29c55a 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 de3fb9d8b638..2c5c64124112 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/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 243148ee1efd..9324d08b15d7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -1073,7 +1073,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = HttpMethod.GET.matches(method); - if (isGet || HttpMethod.HEAD.matches(method)) { + boolean isQuery = HttpMethod.QUERY.matches(method); + if (isGet || isQuery || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; 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 52f9a26cb594..b6443b67632b 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 aa71230ed33b..9fab0a2fa324 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java @@ -265,6 +265,18 @@ public static RequestPredicate OPTIONS(String pattern) { return method(HttpMethod.OPTIONS).and(path(pattern)); } + /** + * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code QUERY} + * and the given {@code pattern} matches against the request path. + * @param pattern the path pattern to match against + * @return a predicate that matches if the request method is QUERY and if the given pattern + * matches against the request path + * @see org.springframework.web.util.pattern.PathPattern + */ + public static RequestPredicate QUERY(String pattern) { + return method(HttpMethod.QUERY).and(path(pattern)); + } + /** * Return a {@code RequestPredicate} that matches if the request's path has the given extension. * @param extension the path extension to match against, ignoring case diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ResourceHandlerFunction.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ResourceHandlerFunction.java index 3ee4a3df6012..1df20298fdfd 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 @@ -40,7 +40,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; @@ -62,6 +62,11 @@ public ServerResponse handle(ServerRequest request) { .headers(headers -> this.headersConsumer.accept(this.resource, headers)) .build(); } + else if (HttpMethod.QUERY.equals(method)) { + return EntityResponse.fromObject(this.resource) + .headers(headers -> this.headersConsumer.accept(this.resource, headers)) + .build(); + } else if (HttpMethod.HEAD.equals(method)) { Resource headResource = new HeadMethodResource(this.resource); return EntityResponse.fromObject(headResource) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java index 6a5c4806b731..7ce41621ec81 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java @@ -229,6 +229,30 @@ public RouterFunctions.Builder OPTIONS(String pattern, RequestPredicate predicat return add(RequestPredicates.OPTIONS(pattern).and(predicate), handlerFunction); } + // QUERY + + @Override + public RouterFunctions.Builder QUERY(HandlerFunction handlerFunction) { + return add(RequestPredicates.method(HttpMethod.QUERY), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction) { + return add(RequestPredicates.method(HttpMethod.QUERY).and(predicate), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(String pattern, HandlerFunction handlerFunction) { + return add(RequestPredicates.QUERY(pattern), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(String pattern, RequestPredicate predicate, + HandlerFunction handlerFunction) { + + return add(RequestPredicates.QUERY(pattern).and(predicate), handlerFunction); + } + // other @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java index 536f3b43d019..acf52880aed2 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 7fd5b7837248..9f538ec2c2b8 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 @@ -165,6 +165,9 @@ private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue 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; } @@ -192,6 +195,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 b98af5617652..46dba7d42652 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 @@ -526,7 +526,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 fda6f3adbbda..170ecb993f7e 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); if (returnStatus == 200) { 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 14cd680e4d98..45f9c740e9bc 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 @@ -148,7 +148,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 201f7ffc9ce0..dcc63e8a0339 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 @@ -920,7 +920,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(); @@ -953,7 +953,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 9c6d9320fda2..ae7eb4262fcf 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 @@ -174,7 +174,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); @@ -183,7 +183,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 75d7eca575f9..8ca74beb2f02 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 @@ -183,7 +183,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 a121a1a9486d..c76f612189f7 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, "/qux", "PATCH,GET,QUERY,HEAD,OPTIONS", new MediaType("foo", "bar")); + testHttpOptions(mapping, "/quid", "QUERY,OPTIONS", null); } @PathPatternsParameterizedTest @@ -569,6 +570,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 3b47c09bc7f9..db554ad6bb2e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -148,6 +148,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 6e203e300be3..0327535a2bfc 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 @@ -115,7 +115,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 43efcda039d8..a28e1f69d19b 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