diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestConformance.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestConformance.java index f72fef2fb9..3c74565e4a 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestConformance.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestConformance.java @@ -38,6 +38,7 @@ import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.message.MessageSupport; import org.apache.hc.core5.http.protocol.HttpContext; @@ -76,6 +77,13 @@ public H2RequestConformance() { public void process(final HttpRequest request, final EntityDetails entity, final HttpContext localContext) throws HttpException, IOException { Args.notNull(request, "HTTP request"); + + if (Method.QUERY.isSame(request.getMethod())) { + if (!request.containsHeader(HttpHeaders.CONTENT_TYPE)) { + throw new ProtocolException("QUERY request must have Content-Type header"); + } + } + for (int i = 0; i < illegalHeaderNames.length; i++) { final String headerName = illegalHeaderNames[i]; if (request.containsHeader(headerName)) { diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestConformance.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestConformance.java index 01fac8c600..e4dc9d3a41 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestConformance.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestConformance.java @@ -36,6 +36,20 @@ class TestH2RequestConformance { + @Test + void testQueryMissingContentType() { + final HttpRequest request = new BasicHttpRequest("QUERY", "/"); + Assertions.assertThrows(ProtocolException.class, + () -> H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create())); + } + + @Test + void testQueryWithContentType() throws Exception { + final HttpRequest request = new BasicHttpRequest("QUERY", "/"); + request.setHeader(HttpHeaders.CONTENT_TYPE, "application/sql"); + H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create()); + } + @Test void testTEAbsent() throws Exception { final HttpRequest request = new BasicHttpRequest("GET", "/"); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/HttpHeaders.java b/httpcore5/src/main/java/org/apache/hc/core5/http/HttpHeaders.java index 69f435b47c..559d3a5cfc 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/HttpHeaders.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/HttpHeaders.java @@ -48,6 +48,13 @@ private HttpHeaders() { public static final String ACCEPT_RANGES = "Accept-Ranges"; + /** + * The RFC 10008 {@code Accept-Query} response header field name. + * + * @since 5.5 + */ + public static final String ACCEPT_QUERY = "Accept-Query"; + /** * The CORS {@code Access-Control-Allow-Credentials} response header field name. */ diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/Method.java b/httpcore5/src/main/java/org/apache/hc/core5/http/Method.java index 787fb405fc..a2c9730428 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/Method.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/Method.java @@ -29,7 +29,6 @@ import java.util.Locale; -import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.util.Args; /** @@ -104,8 +103,6 @@ public enum Method { * * @since 5.4 */ - @Experimental - //("QUERY method is still in DRAFT status") QUERY(true, true); private final boolean safe; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilder.java index dc4a6c8e1d..bd112e7552 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilder.java @@ -33,7 +33,6 @@ import java.util.Arrays; import java.util.List; -import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.Header; @@ -129,7 +128,6 @@ public static ClassicRequestBuilder head(final String uri) { * * @since 5.4 */ - @Experimental public static ClassicRequestBuilder query() { return new ClassicRequestBuilder(Method.QUERY); } @@ -142,7 +140,6 @@ public static ClassicRequestBuilder query() { * * @since 5.4 */ - @Experimental public static ClassicRequestBuilder query(final URI uri) { return new ClassicRequestBuilder(Method.QUERY, uri); } @@ -155,7 +152,6 @@ public static ClassicRequestBuilder query(final URI uri) { * * @since 5.4 */ - @Experimental public static ClassicRequestBuilder query(final String uri) { return new ClassicRequestBuilder(Method.QUERY, uri); } @@ -408,7 +404,7 @@ public ClassicHttpRequest build() { final String method = getMethod(); final List parameters = getParameters(); if (parameters != null && !parameters.isEmpty()) { - if (entityCopy == null && (Method.POST.isSame(method) || Method.PUT.isSame(method))) { + if (entityCopy == null && (Method.POST.isSame(method) || Method.PUT.isSame(method) || Method.QUERY.isSame(method))) { entityCopy = HttpEntities.createUrlEncoded(parameters, getCharset()); } else { try { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java index 7b16f97747..1bd846af7e 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java @@ -33,7 +33,6 @@ import java.util.Arrays; import java.util.List; -import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpHost; @@ -128,7 +127,6 @@ public static AsyncRequestBuilder head(final String uri) { * * @since 5.4 */ - @Experimental public static AsyncRequestBuilder query() { return new AsyncRequestBuilder(Method.QUERY); } @@ -141,7 +139,6 @@ public static AsyncRequestBuilder query() { * * @since 5.4 */ - @Experimental public static AsyncRequestBuilder query(final URI uri) { return new AsyncRequestBuilder(Method.QUERY, uri); } @@ -154,7 +151,6 @@ public static AsyncRequestBuilder query(final URI uri) { * * @since 5.4 */ - @Experimental public static AsyncRequestBuilder query(final String uri) { return new AsyncRequestBuilder(Method.QUERY, uri); } @@ -383,7 +379,7 @@ public AsyncRequestProducer build() { final List parameters = getParameters(); if (parameters != null && !parameters.isEmpty()) { final Charset charset = getCharset(); - if (entityProducerCopy == null && (Method.POST.isSame(method) || Method.PUT.isSame(method))) { + if (entityProducerCopy == null && (Method.POST.isSame(method) || Method.PUT.isSame(method) || Method.QUERY.isSame(method))) { final String content = WWWFormCodec.format( parameters, charset != null ? charset : ContentType.APPLICATION_FORM_URLENCODED.getCharset()); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestConformance.java b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestConformance.java index fd931b28d8..f26f6c3d48 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestConformance.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestConformance.java @@ -35,6 +35,8 @@ import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.MisdirectedRequestException; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.URIScheme; @@ -66,6 +68,12 @@ public void process(final HttpRequest request, final EntityDetails entity, final throws HttpException, IOException { Args.notNull(request, "HTTP request"); + if (Method.QUERY.isSame(request.getMethod())) { + if (!request.containsHeader(HttpHeaders.CONTENT_TYPE)) { + throw new ProtocolException("QUERY request must have Content-Type header"); + } + } + if (TextUtils.isBlank(request.getScheme())) { throw new ProtocolException("Request scheme is not set"); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/support/BasicRequestBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/support/BasicRequestBuilder.java index 2d26205108..bec92eede6 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/support/BasicRequestBuilder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/support/BasicRequestBuilder.java @@ -33,7 +33,6 @@ import java.util.Arrays; import java.util.List; -import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpRequest; @@ -112,7 +111,6 @@ public static BasicRequestBuilder head(final String uri) { * * @since 5.4 */ - @Experimental public static BasicRequestBuilder query() { return new BasicRequestBuilder(Method.QUERY); } @@ -125,7 +123,6 @@ public static BasicRequestBuilder query() { * * @since 5.4 */ - @Experimental public static BasicRequestBuilder query(final URI uri) { return new BasicRequestBuilder(Method.QUERY, uri); } @@ -138,7 +135,6 @@ public static BasicRequestBuilder query(final URI uri) { * * @since 5.4 */ - @Experimental public static BasicRequestBuilder query(final String uri) { return new BasicRequestBuilder(Method.QUERY, uri); } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilderTest.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilderTest.java index 685c42bdf8..c5331f084f 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilderTest.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilderTest.java @@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.io.IOException; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; @@ -42,6 +43,7 @@ import org.apache.hc.core5.http.HttpVersion; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.message.BasicNameValuePair; @@ -249,4 +251,23 @@ void builderTraceThrowsIllegalStateException() { .build()); } + @Test + void builderQuery() throws IOException { + final ClassicHttpRequest classicHttpRequest = ClassicRequestBuilder.query() + .setHttpHost(new HttpHost("httpbin.org")) + .setPath("/theUri") + .addParameter("param1", "value1") + .addParameter("param2", "value2") + .build(); + + assertAll("QUERY builder tests", + () -> assertEquals(Method.QUERY.name(), classicHttpRequest.getMethod()), + () -> assertEquals("httpbin.org", classicHttpRequest.getAuthority().getHostName()), + () -> assertEquals("/theUri", classicHttpRequest.getPath()), + () -> assertNotNull(classicHttpRequest.getEntity()), + () -> assertEquals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType(), ContentType.parse(classicHttpRequest.getEntity().getContentType()).getMimeType()), + () -> assertEquals("param1=value1¶m2=value2", EntityUtils.toString(classicHttpRequest.getEntity())) + ); + } + } \ No newline at end of file diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java index d0c3d4104b..5d37b6ad08 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java @@ -1043,6 +1043,30 @@ void testRequestConformance() { Assertions.assertDoesNotThrow(() -> interceptor.process(request, request.getEntity(), context)); } + @Test + void testRequestConformanceQueryMissingContentType() { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.QUERY, "/"); + request.setScheme("http"); + request.setAuthority(new URIAuthority("somehost", 8888)); + request.setPath("/path"); + final RequestConformance interceptor = new RequestConformance(); + Assertions.assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + void testRequestConformanceQueryWithContentType() { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.QUERY, "/"); + request.setScheme("http"); + request.setAuthority(new URIAuthority("somehost", 8888)); + request.setPath("/path"); + request.setHeader(HttpHeaders.CONTENT_TYPE, "application/jsonpath"); + final RequestConformance interceptor = new RequestConformance(); + Assertions.assertDoesNotThrow(() -> interceptor.process(request, request.getEntity(), context)); + } + @Test void testRequestConformanceSchemeMissing() { final HttpCoreContext context = HttpCoreContext.create();