Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ void options() {
RestTestClientTests.this.client.options().uri("/test")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals("Allow", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS")
.expectHeader().valueEquals("Allow", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,QUERY")
.expectBody().isEmpty();
}

Expand Down
27 changes: 27 additions & 0 deletions spring-web/src/main/java/org/springframework/http/HttpHeaders.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ public class HttpHeaders implements Serializable {
* @see <a href="https://tools.ietf.org/html/rfc5789#section-3.1">Section 3.1 of RFC 5789</a>
*/
public static final String ACCEPT_PATCH = "Accept-Patch";
/**
* The HTTP {@code Accept-Query} header field name.
* @since 7.1
* @see <a href="https://www.rfc-editor.org/rfc/rfc10008.html#section-3">Section 3 of RFC 10008</a>
*/
public static final String ACCEPT_QUERY = "Accept-Query";
/**
* The HTTP {@code Accept-Ranges} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7233#section-2.3">Section 5.3.5 of RFC 7233</a>
Expand Down Expand Up @@ -648,6 +654,27 @@ public List<MediaType> getAcceptPatch() {
return MediaType.parseMediaTypes(get(ACCEPT_PATCH));
}

/**
* Set the list of acceptable {@linkplain MediaType media types} for
* {@code QUERY} methods, as specified by the {@code Accept-Query} header.
* @since 7.1
*/
public void setAcceptQuery(List<MediaType> 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.
* <p>Returns an empty list when the acceptable media types are unspecified.
* @since 7.1
*/
public List<MediaType> getAcceptQuery() {
return MediaType.parseMediaTypes(get(ACCEPT_QUERY));
}



/**
* Set the (new) value of the {@code Access-Control-Allow-Credentials} response header.
*/
Expand Down
26 changes: 17 additions & 9 deletions spring-web/src/main/java/org/springframework/http/HttpMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,25 @@ public final class HttpMethod implements Comparable<HttpMethod>, Serializable {

/**
* The HTTP method {@code GET}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3">HTTP 1.1, section 9.3</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.1">HTTP Semantics, section 9.3.1</a>
*/
public static final HttpMethod GET = new HttpMethod("GET");

/**
* The HTTP method {@code HEAD}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4">HTTP 1.1, section 9.4</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.2">HTTP Semantics, section 9.3.2</a>
*/
public static final HttpMethod HEAD = new HttpMethod("HEAD");

/**
* The HTTP method {@code POST}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5">HTTP 1.1, section 9.5</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.3">HTTP Semantics, section 9.3.3</a>
*/
public static final HttpMethod POST = new HttpMethod("POST");

/**
* The HTTP method {@code PUT}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6">HTTP 1.1, section 9.6</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.4">HTTP Semantics, section 9.3.4</a>
*/
public static final HttpMethod PUT = new HttpMethod("PUT");

Expand All @@ -69,23 +69,30 @@ public final class HttpMethod implements Comparable<HttpMethod>, Serializable {

/**
* The HTTP method {@code DELETE}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7">HTTP 1.1, section 9.7</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.5">HTTP Semantics, section 9.3.5</a>
*/
public static final HttpMethod DELETE = new HttpMethod("DELETE");

/**
* The HTTP method {@code OPTIONS}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2">HTTP 1.1, section 9.2</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.7">HTTP Semantics, section 9.3.7</a>
*/
public static final HttpMethod OPTIONS = new HttpMethod("OPTIONS");

/**
* The HTTP method {@code TRACE}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8">HTTP 1.1, section 9.8</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.8">HTTP Semantics, section 9.3.8</a>
*/
public static final HttpMethod TRACE = new HttpMethod("TRACE");

private static final HttpMethod[] values = new HttpMethod[] { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE };
/**
* The HTTP method {@code QUERY}.
* @since 7.1
* @see <a href="https://www.rfc-editor.org/rfc/rfc10008.html#section-2">The HTTP QUERY Method, section 2</a>
*/
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;
Expand All @@ -99,7 +106,7 @@ private HttpMethod(String name) {
* Returns an array containing the standard HTTP methods. Specifically,
* this method returns an array containing {@link #GET}, {@link #HEAD},
* {@link #POST}, {@link #PUT}, {@link #PATCH}, {@link #DELETE},
* {@link #OPTIONS}, and {@link #TRACE}.
* {@link #OPTIONS}, {@link #TRACE}, and {@link #QUERY}.
*
* <p>Note that the returned value does not include any HTTP methods defined
* in WebDav.
Expand Down Expand Up @@ -137,6 +144,7 @@ public static HttpMethod valueOf(String method) {
case "DELETE" -> DELETE;
case "OPTIONS" -> OPTIONS;
case "TRACE" -> TRACE;
case "QUERY" -> QUERY;
default -> new HttpMethod(method);
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -300,6 +301,9 @@ else if (HttpMethod.OPTIONS.equals(httpMethod)) {
else if (HttpMethod.TRACE.equals(httpMethod)) {
return new HttpTrace(uri);
}
else if (HttpMethod.QUERY.equals(httpMethod)) {
return new HttpUriRequestBase(HttpMethod.QUERY.name(), uri);
}
throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ public HttpHeaders getHeaders() {
if (HttpMethod.PATCH.equals(this.httpMethod)) {
headers.setAcceptPatch(getSupportedMediaTypes());
}
else if (HttpMethod.QUERY.equals(this.httpMethod)) {
headers.setAcceptQuery(getSupportedMediaTypes());
}
return headers;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
* {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation.
*
* <p>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.
Expand All @@ -39,7 +39,7 @@
*/
public enum RequestMethod {

GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, QUERY;


/**
Expand All @@ -60,6 +60,7 @@ public enum RequestMethod {
case "DELETE" -> DELETE;
case "OPTIONS" -> OPTIONS;
case "TRACE" -> TRACE;
case "QUERY" -> QUERY;
default -> null;
};
}
Expand Down Expand Up @@ -92,6 +93,7 @@ public HttpMethod asHttpMethod() {
case DELETE -> HttpMethod.DELETE;
case OPTIONS -> HttpMethod.OPTIONS;
case TRACE -> HttpMethod.TRACE;
case QUERY -> HttpMethod.QUERY;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
import org.springframework.util.StringUtils;

/**
* {@code Filter} that parses form data for HTTP PUT, PATCH, and DELETE requests
* {@code Filter} that parses form data for HTTP PUT, PATCH, DELETE, and QUERY requests
* and exposes it as Servlet request parameters. By default, the Servlet spec
* only requires this for HTTP POST.
*
Expand All @@ -56,7 +56,7 @@
*/
public class FormContentFilter extends OncePerRequestFilter {

private static final List<String> HTTP_METHODS = Arrays.asList("PUT", "PATCH", "DELETE");
private static final List<String> HTTP_METHODS = Arrays.asList("PUT", "PATCH", "DELETE", "QUERY");

private FormHttpMessageConverter formConverter = new FormHttpMessageConverter();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
* is to use a normal POST with an additional hidden form field ({@code _method})
* to pass the "real" HTTP method along. This filter reads that parameter and changes
* the {@link HttpServletRequestWrapper#getMethod()} return value accordingly.
* Only {@code "PUT"}, {@code "DELETE"} and {@code "PATCH"} HTTP methods are allowed.
* Only {@code "PUT"}, {@code "DELETE"}, {@code "PATCH"}, and {@code "QUERY"} HTTP methods are allowed.
*
* <p>The name of the request parameter defaults to {@code _method}, but can be
* adapted via the {@link #setMethodParam(String) methodParam} property.
Expand All @@ -55,7 +55,7 @@
public class HiddenHttpMethodFilter extends OncePerRequestFilter {

private static final List<String> ALLOWED_METHODS =
List.of(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name());
List.of(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name(), HttpMethod.QUERY.name());

/** Default method parameter: {@code _method}. */
public static final String DEFAULT_METHOD_PARAM = "_method";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
public class HiddenHttpMethodFilter implements WebFilter {

private static final List<HttpMethod> ALLOWED_METHODS =
List.of(HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.PATCH);
List.of(HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.PATCH, HttpMethod.QUERY);

/** Default name of the form parameter with the HTTP method to use. */
public static final String DEFAULT_METHOD_PARAMETER_NAME = "_method";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ public HttpHeaders getHeaders() {
if (this.method == HttpMethod.PATCH) {
headers.setAcceptPatch(this.supportedMediaTypes);
}
else if (this.method == HttpMethod.QUERY) {
headers.setAcceptQuery(this.supportedMediaTypes);
}
return headers;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class RequestMethodTests {

@Test
void resolveString() {
String[] methods = new String[]{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE"};
String[] methods = new String[]{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE", "QUERY"};
for (String httpMethod : methods) {
RequestMethod requestMethod = RequestMethod.resolve(httpMethod);
assertThat(requestMethod).isNotNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ void optionsForAllow(ClientHttpRequestFactory clientHttpRequestFactory) {
setUpClient(clientHttpRequestFactory);

Set<HttpMethod> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ void setup() {


@Test
void wrapPutPatchAndDeleteOnly() throws Exception {
void wrapPutPatchQueryAndDeleteOnly() throws Exception {
for (HttpMethod method : HttpMethod.values()) {
MockHttpServletRequest request = new MockHttpServletRequest(method.name(), "/");
request.setContent("foo=bar".getBytes(StandardCharsets.ISO_8859_1));
request.setContentType("application/x-www-form-urlencoded; charset=ISO-8859-1");
this.filterChain = new MockFilterChain();
this.filter.doFilter(request, this.response, this.filterChain);
if (method == HttpMethod.PUT || method == HttpMethod.PATCH || method == HttpMethod.DELETE) {
if (method == HttpMethod.PUT || method == HttpMethod.PATCH || method == HttpMethod.DELETE || method == HttpMethod.QUERY) {
assertThat(this.filterChain.getRequest()).isNotSameAs(request);
}
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ public static RequestMappingPredicate headMapping(String... path) {
}



public static class ModelAttributePredicate implements Predicate<MethodParameter> {

private String name;
Expand Down
Loading
Loading