From 640bb52c0af8478caf43bffaf9cfb03da739407d Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Tue, 16 Jun 2026 16:29:30 -0300 Subject: [PATCH 1/2] Add support for HTTP QUERY method --- .../async/methods/SimpleRequestBuilder.java | 14 +++- .../http/classic/methods/HttpQuery.java | 66 +++++++++++++++++++ .../methods/TestSimpleMessageBuilders.java | 21 ++++++ .../classic/methods/TestHttpRequestBase.java | 19 +++++- 4 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/classic/methods/HttpQuery.java diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/SimpleRequestBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/SimpleRequestBuilder.java index a6acd70477..ee0360b05e 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/SimpleRequestBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/SimpleRequestBuilder.java @@ -170,6 +170,18 @@ public static SimpleRequestBuilder delete(final String uri) { return new SimpleRequestBuilder(Method.DELETE, uri); } + public static SimpleRequestBuilder query() { + return new SimpleRequestBuilder(Method.QUERY); + } + + public static SimpleRequestBuilder query(final URI uri) { + return new SimpleRequestBuilder(Method.QUERY, uri); + } + + public static SimpleRequestBuilder query(final String uri) { + return new SimpleRequestBuilder(Method.QUERY, uri); + } + public static SimpleRequestBuilder trace() { return new SimpleRequestBuilder(Method.TRACE); } @@ -374,7 +386,7 @@ public SimpleHttpRequest build() { final List parameters = getParameters(); if (parameters != null && !parameters.isEmpty()) { final Charset charsetCopy = getCharset(); - if (bodyCopy == null && (Method.POST.isSame(method) || Method.PUT.isSame(method))) { + if (bodyCopy == null && (Method.POST.isSame(method) || Method.PUT.isSame(method) || Method.QUERY.isSame(method))) { final String content = WWWFormCodec.format( parameters, charsetCopy != null ? charsetCopy : ContentType.APPLICATION_FORM_URLENCODED.getCharset()); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/classic/methods/HttpQuery.java b/httpclient5/src/main/java/org/apache/hc/client5/http/classic/methods/HttpQuery.java new file mode 100644 index 0000000000..2bd1c782fa --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/classic/methods/HttpQuery.java @@ -0,0 +1,66 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.classic.methods; + +import java.net.URI; + +/** + * HTTP QUERY method. + * + * @since 5.7 + */ +public class HttpQuery extends HttpUriRequestBase { + + private static final long serialVersionUID = 1L; + + /** + * The method name {@value}. + */ + public static final String METHOD_NAME = "QUERY"; + + /** + * Constructs a new instance initialized with the given URI. + * + * @param uri a non-null request URI. + * @throws IllegalArgumentException if the uri is null. + */ + public HttpQuery(final URI uri) { + super(METHOD_NAME, uri); + } + + /** + * Constructs a new instance initialized with the given URI. + * + * @param uri a non-null request URI. + * @throws IllegalArgumentException if the uri is invalid. + */ + public HttpQuery(final String uri) { + this(URI.create(uri)); + } + +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/TestSimpleMessageBuilders.java b/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/TestSimpleMessageBuilders.java index cc326eb469..c13a305609 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/TestSimpleMessageBuilders.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/TestSimpleMessageBuilders.java @@ -273,4 +273,25 @@ void testPostParameters() { Assertions.assertEquals("p1=v1&p2=v2&p3=v3&p3=v3.1", request.getBody().getBodyText()); } + @Test + void testQueryParameters() { + final SimpleRequestBuilder builder = SimpleRequestBuilder.query(URI.create("https://host:3456/stuff?p0=p0")); + builder.addParameter("p1", "v1"); + builder.addParameters(new BasicNameValuePair("p2", "v2"), new BasicNameValuePair("p3", "v3")); + builder.addParameter(new BasicNameValuePair("p3", "v3.1")); + Assertions.assertEquals("QUERY", builder.getMethod()); + Assertions.assertEquals("https", builder.getScheme()); + Assertions.assertEquals(new URIAuthority("host", 3456), builder.getAuthority()); + Assertions.assertEquals("/stuff?p0=p0", builder.getPath()); + NameValuePairsMatcher.assertSame(builder.getParameters(), + new BasicNameValuePair("p1", "v1"), new BasicNameValuePair("p2", "v2"), + new BasicNameValuePair("p3", "v3"), new BasicNameValuePair("p3", "v3.1")); + final SimpleHttpRequest request = builder.build(); + Assertions.assertEquals("/stuff?p0=p0", request.getPath()); + Assertions.assertNotNull(request.getBody()); + ContentTypeMatcher.assertSameMimeType(request.getBody().getContentType(), + ContentType.APPLICATION_FORM_URLENCODED); + Assertions.assertEquals("p1=v1&p2=v2&p3=v3&p3=v3.1", request.getBody().getBodyText()); + } + } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/classic/methods/TestHttpRequestBase.java b/httpclient5/src/test/java/org/apache/hc/client5/http/classic/methods/TestHttpRequestBase.java index 7854d8acf2..819ebf143a 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/classic/methods/TestHttpRequestBase.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/classic/methods/TestHttpRequestBase.java @@ -108,6 +108,13 @@ void testBasicHttpDeleteMethodProperties() throws Exception { Assertions.assertEquals(new URI(HOT_URL), httpDelete.getUri()); } + @Test + void testBasicQueryMethodProperties() throws Exception { + final HttpQuery httpQuery = new HttpQuery(HOT_URL); + Assertions.assertEquals("QUERY", httpQuery.getMethod()); + Assertions.assertEquals(new URI(HOT_URL), httpQuery.getUri()); + } + @Test void testGetMethodEmptyURI() throws Exception { @@ -158,6 +165,12 @@ void testDeleteMethodEmptyURI() throws Exception { Assertions.assertEquals(new URI("/"), httpDelete.getUri()); } + @Test + void testQueryMethodEmptyURI() throws Exception { + final HttpQuery httpQuery = new HttpQuery(""); + Assertions.assertEquals(new URI("/"), httpQuery.getUri()); + } + @Test void testTraceMethodSetEntity() { @@ -169,15 +182,15 @@ void testTraceMethodSetEntity() { @Test void testOptionMethodGetAllowedMethods() { final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK, "OK"); - response.addHeader("Allow", "GET, HEAD"); + response.addHeader("Allow", "GET, HEAD, QUERY"); response.addHeader("Allow", "DELETE"); response.addHeader("Content-Length", "128"); final HttpOptions httpOptions = new HttpOptions(""); final Set methods = httpOptions.getAllowedMethods(response); assertAll("Must all pass", () -> assertFalse(methods.isEmpty()), - () -> assertEquals(3, methods.size()), - () -> assertTrue(methods.containsAll(Stream.of("HEAD", "DELETE", "GET") + () -> assertEquals(4, methods.size()), + () -> assertTrue(methods.containsAll(Stream.of("HEAD", "DELETE", "GET", "QUERY") .collect(Collectors.toCollection(HashSet::new)))) ); } From 18b0b6e368d51e339d2c54e021bd0142edc8fa06 Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Tue, 16 Jun 2026 21:01:10 -0300 Subject: [PATCH 2/2] Add HTTP QUERY method caching support --- .../http/impl/cache/CacheKeyGenerator.java | 79 ++++++++++++++-- .../impl/cache/CacheableRequestPolicy.java | 2 +- .../cache/CachedHttpResponseGenerator.java | 2 +- .../impl/cache/ResponseCachingPolicy.java | 4 +- .../impl/cache/TestCacheKeyGenerator.java | 27 ++++++ .../http/impl/cache/TestCachingExecChain.java | 93 +++++++++++++++++++ .../hc/client5/http/fluent/Request.java | 14 +++ .../hc/client5/http/fluent/TestRequest.java | 1 + .../TestDefaultHttpRequestRetryStrategy.java | 8 ++ 9 files changed, 220 insertions(+), 10 deletions(-) diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheKeyGenerator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheKeyGenerator.java index e15b06ca85..8e230b0bbb 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheKeyGenerator.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheKeyGenerator.java @@ -29,6 +29,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; @@ -41,18 +43,25 @@ import java.util.stream.StreamSupport; import org.apache.hc.client5.http.cache.HttpCacheEntry; -import org.apache.hc.core5.annotation.Contract; -import org.apache.hc.core5.annotation.Internal; -import org.apache.hc.core5.annotation.ThreadingBehavior; -import org.apache.hc.core5.function.Resolver; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HeaderElement; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.MessageHeaders; import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.function.Resolver; import org.apache.hc.core5.http.message.BasicHeaderElementIterator; import org.apache.hc.core5.http.message.BasicHeaderValueFormatter; import org.apache.hc.core5.http.message.BasicNameValuePair; @@ -174,9 +183,24 @@ public static URI normalize(final String requestUri) { * @return cache key */ public String generateKey(final URI requestUri) { + return generateKey(requestUri, null); + } + + /** + * Computes a key for the given request {@link URI} and body hash that can + * be used as a unique identifier for cached resources. The URI is + * expected to be in an absolute form. + * + * @param requestUri request URI + * @param bodyHash hash of the request body + * @return cache key + */ + public String generateKey(final URI requestUri, final String bodyHash) { try { final URI normalizeRequestUri = normalize(requestUri); - return normalizeRequestUri.toASCIIString(); + final String uriString = normalizeRequestUri.toASCIIString(); + // Using a Record Separator to make it easy to spot + return bodyHash == null ? uriString : uriString + '\u001e' + bodyHash; } catch (final URISyntaxException ex) { return requestUri.toASCIIString(); } @@ -192,13 +216,56 @@ public String generateKey(final URI requestUri) { */ public String generateKey(final HttpHost host, final HttpRequest request) { final String s = getRequestUri(host, request); + final String hash = Method.QUERY.isSame(request.getMethod()) ? getBodyHash(request) : null; try { - return generateKey(new URI(s)); + return generateKey(new URI(s), hash); } catch (final URISyntaxException ex) { return s; } } + private static byte[] getRequestBodyBytes(final HttpRequest request) { + if (request instanceof SimpleHttpRequest) { + return ((SimpleHttpRequest) request).getBodyBytes(); + } + if (request instanceof ClassicHttpRequest) { + final HttpEntity entity = ((ClassicHttpRequest) request).getEntity(); + if (entity != null) { + try { + if (entity.isRepeatable()) { + final byte[] bytes = EntityUtils.toByteArray(entity); + ((ClassicHttpRequest) request).setEntity(new ByteArrayEntity(bytes, ContentType.parse(entity.getContentType()))); + return bytes; + } + } catch (final Exception ignore) { + } + } + } + return null; + } + + private static String getBodyHash(final HttpRequest request) { + final byte[] bodyBytes = getRequestBodyBytes(request); + if (bodyBytes == null || bodyBytes.length == 0) { + return ""; + } + try { + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + final byte[] hashBytes = digest.digest(bodyBytes); + final StringBuilder hexString = new StringBuilder(); + for (final byte b : hashBytes) { + final String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (final NoSuchAlgorithmException e) { + return ""; + } + } + /** * Returns all variant names contained in {@literal VARY} headers of the given message. * diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheableRequestPolicy.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheableRequestPolicy.java index f1fbdb6a7f..3006224763 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheableRequestPolicy.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheableRequestPolicy.java @@ -57,7 +57,7 @@ public boolean canBeServedFromCache(final RequestCacheControl cacheControl, fina return false; } - if (!Method.GET.isSame(method) && !Method.HEAD.isSame(method)) { + if (!Method.GET.isSame(method) && !Method.HEAD.isSame(method) && !Method.QUERY.isSame(method)) { if (LOG.isDebugEnabled()) { LOG.debug("{} request cannot be served from cache", method); } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java index 91b36b63a6..75dd9cf826 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java @@ -156,7 +156,7 @@ private void generateContentLength(final HttpResponse response, final byte[] bod } private boolean responseShouldContainEntity(final HttpRequest request, final HttpCacheEntry cacheEntry) { - return Method.GET.isSame(request.getMethod()) && cacheEntry.getResource() != null; + return (Method.GET.isSame(request.getMethod()) || Method.QUERY.isSame(request.getMethod())) && cacheEntry.getResource() != null; } } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java index f5d25c35c6..5c5c3af929 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java @@ -98,9 +98,9 @@ public boolean isResponseCacheable(final ResponseCacheControl cacheControl, fina return false; } - // Presently only GET and HEAD methods are supported + // Presently only GET, HEAD and QUERY methods are supported final String httpMethod = request.getMethod(); - if (!Method.GET.isSame(httpMethod) && !Method.HEAD.isSame(httpMethod)) { + if (!Method.GET.isSame(httpMethod) && !Method.HEAD.isSame(httpMethod) && !Method.QUERY.isSame(httpMethod)) { if (LOG.isDebugEnabled()) { LOG.debug("{} method response is not cacheable", httpMethod); } diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheKeyGenerator.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheKeyGenerator.java index 8fcb19553e..aadb2c51cb 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheKeyGenerator.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheKeyGenerator.java @@ -36,13 +36,17 @@ import org.apache.hc.client5.http.cache.HttpCacheEntry; import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.message.BasicHeaderIterator; import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.support.BasicRequestBuilder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -404,4 +408,27 @@ void testGetVariantKeyFromCachedResponse() { Assertions.assertEquals("{accept-encoding=text%2Fplain&user-agent=agent1}", extractor.generateVariantKey(request, entry2)); } + @Test + void testQueryKeyGenerationWithBody() throws Exception { + final HttpHost host = new HttpHost("http", "www.example.com", -1); + + // SimpleHttpRequest with body + final SimpleHttpRequest req1 = SimpleHttpRequest.create("QUERY", "/stuff"); + req1.setBody("my-query-1", ContentType.TEXT_PLAIN); + final String key1 = extractor.generateKey(host, req1); + + final SimpleHttpRequest req2 = SimpleHttpRequest.create("QUERY", "/stuff"); + req2.setBody("my-query-2", ContentType.TEXT_PLAIN); + final String key2 = extractor.generateKey(host, req2); + + Assertions.assertNotEquals(key1, key2); + Assertions.assertTrue(key1.contains("\u001e")); + + // ClassicHttpRequest with body + final BasicClassicHttpRequest req3 = new BasicClassicHttpRequest("QUERY", "/stuff"); + req3.setEntity(new StringEntity("my-query-1", ContentType.TEXT_PLAIN)); + final String key3 = extractor.generateKey(host, req3); + Assertions.assertEquals(key1, key3); + } + } diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java index c76b1ece05..02acdb6db7 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java @@ -57,8 +57,10 @@ import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.InputStreamEntity; +import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; @@ -1285,4 +1287,95 @@ void testNoCacheFieldsRevalidation() throws Exception { Mockito.verify(mockExecChain, Mockito.times(5)).proceed(Mockito.any(), Mockito.any()); } + @Test + void testQueryRequestsGoIntoCacheAndMatchBody() throws Exception { + final ClassicHttpRequest req1 = new BasicClassicHttpRequest("QUERY", "/stuff"); + req1.setEntity(new StringEntity("query-body-1", ContentType.TEXT_PLAIN)); + + final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(200, "OK"); + resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now())); + resp1.setHeader("Cache-Control", "max-age=3600"); + resp1.setHeader("Content-Type", "text/plain"); + resp1.setEntity(new StringEntity("response-body-1", ContentType.TEXT_PLAIN)); + + Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1); + + // First execution (Cache Miss, goes to backend) + final ClassicHttpResponse result1 = execute(req1); + Assertions.assertEquals(200, result1.getCode()); + Assertions.assertEquals("response-body-1", EntityUtils.toString(result1.getEntity())); + + // Second execution with identical QUERY request (Cache Hit) + final ClassicHttpRequest req2 = new BasicClassicHttpRequest("QUERY", "/stuff"); + req2.setEntity(new StringEntity("query-body-1", ContentType.TEXT_PLAIN)); + final ClassicHttpResponse result2 = execute(req2); + Assertions.assertEquals(200, result2.getCode()); + Assertions.assertEquals("response-body-1", EntityUtils.toString(result2.getEntity())); + Assertions.assertEquals(CacheResponseStatus.CACHE_HIT, context.getCacheResponseStatus()); + + // Third execution with different QUERY request body (Cache Miss, goes to backend) + final ClassicHttpRequest req3 = new BasicClassicHttpRequest("QUERY", "/stuff"); + req3.setEntity(new StringEntity("query-body-2", ContentType.TEXT_PLAIN)); + + final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(200, "OK"); + resp3.setHeader("Date", DateUtils.formatStandardDate(Instant.now())); + resp3.setHeader("Cache-Control", "max-age=3600"); + resp3.setHeader("Content-Type", "text/plain"); + resp3.setEntity(new StringEntity("response-body-2", ContentType.TEXT_PLAIN)); + + Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3); + + final ClassicHttpResponse result3 = execute(req3); + Assertions.assertEquals(200, result3.getCode()); + Assertions.assertEquals("response-body-2", EntityUtils.toString(result3.getEntity())); + Assertions.assertEquals(CacheResponseStatus.CACHE_MISS, context.getCacheResponseStatus()); + } + + @Test + void testQueryAndGetRequestsCachedSeparately() throws Exception { + // 1. GET request + final ClassicHttpRequest getReq = new BasicClassicHttpRequest("GET", "/stuff"); + final ClassicHttpResponse getResp = new BasicClassicHttpResponse(200, "OK"); + getResp.setHeader("Date", DateUtils.formatStandardDate(Instant.now())); + getResp.setHeader("Cache-Control", "max-age=3600"); + getResp.setHeader("Content-Type", "text/plain"); + getResp.setEntity(new StringEntity("get-response-content", ContentType.TEXT_PLAIN)); + + Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(getResp); + + // Execute GET (Cache Miss) + final ClassicHttpResponse getResult1 = execute(getReq); + Assertions.assertEquals("get-response-content", EntityUtils.toString(getResult1.getEntity())); + Assertions.assertEquals(CacheResponseStatus.CACHE_MISS, context.getCacheResponseStatus()); + + // 2. QUERY request + final ClassicHttpRequest queryReq = new BasicClassicHttpRequest("QUERY", "/stuff"); + queryReq.setEntity(new StringEntity("query-body", ContentType.TEXT_PLAIN)); + + final ClassicHttpResponse queryResp = new BasicClassicHttpResponse(200, "OK"); + queryResp.setHeader("Date", DateUtils.formatStandardDate(Instant.now())); + queryResp.setHeader("Cache-Control", "max-age=3600"); + queryResp.setHeader("Content-Type", "text/plain"); + queryResp.setEntity(new StringEntity("query-response-content", ContentType.TEXT_PLAIN)); + + Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(queryResp); + + // Execute QUERY (Cache Miss) + final ClassicHttpResponse queryResult1 = execute(queryReq); + Assertions.assertEquals("query-response-content", EntityUtils.toString(queryResult1.getEntity())); + Assertions.assertEquals(CacheResponseStatus.CACHE_MISS, context.getCacheResponseStatus()); + + // 3. Re-execute GET (should Cache Hit with GET content) + final ClassicHttpResponse getResult2 = execute(getReq); + Assertions.assertEquals("get-response-content", EntityUtils.toString(getResult2.getEntity())); + Assertions.assertEquals(CacheResponseStatus.CACHE_HIT, context.getCacheResponseStatus()); + + // 4. Re-execute QUERY (should Cache Hit with QUERY content) + final ClassicHttpResponse queryResult2 = execute(queryReq); + Assertions.assertEquals("query-response-content", EntityUtils.toString(queryResult2.getEntity())); + Assertions.assertEquals(CacheResponseStatus.CACHE_HIT, context.getCacheResponseStatus()); + } + } + + diff --git a/httpclient5-fluent/src/main/java/org/apache/hc/client5/http/fluent/Request.java b/httpclient5-fluent/src/main/java/org/apache/hc/client5/http/fluent/Request.java index afc3900397..6eba5bac5b 100644 --- a/httpclient5-fluent/src/main/java/org/apache/hc/client5/http/fluent/Request.java +++ b/httpclient5-fluent/src/main/java/org/apache/hc/client5/http/fluent/Request.java @@ -170,6 +170,20 @@ public static Request options(final String uri) { return new Request(new BasicClassicHttpRequest(Method.OPTIONS, uri)); } + /** + * @since 5.7 + */ + public static Request query(final URI uri) { + return new Request(new BasicClassicHttpRequest(Method.QUERY, uri)); + } + + /** + * @since 5.7 + */ + public static Request query(final String uri) { + return new Request(new BasicClassicHttpRequest(Method.QUERY, uri)); + } + Request(final ClassicHttpRequest request) { super(); this.request = request; diff --git a/httpclient5-fluent/src/test/java/org/apache/hc/client5/http/fluent/TestRequest.java b/httpclient5-fluent/src/test/java/org/apache/hc/client5/http/fluent/TestRequest.java index 96693e6648..1e3fdc68d1 100644 --- a/httpclient5-fluent/src/test/java/org/apache/hc/client5/http/fluent/TestRequest.java +++ b/httpclient5-fluent/src/test/java/org/apache/hc/client5/http/fluent/TestRequest.java @@ -51,6 +51,7 @@ static Stream data() { Arguments.of("patch", "PATCH"), Arguments.of("post", "POST"), Arguments.of("put", "PUT"), + Arguments.of("query", "QUERY"), Arguments.of("trace", "TRACE") ); } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultHttpRequestRetryStrategy.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultHttpRequestRetryStrategy.java index bdc8e94235..0e335cbcda 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultHttpRequestRetryStrategy.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultHttpRequestRetryStrategy.java @@ -35,6 +35,7 @@ import java.time.temporal.ChronoUnit; import javax.net.ssl.SSLException; import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpQuery; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.utils.DateUtils; @@ -188,6 +189,13 @@ void retryOnNonAbortedRequests() { Assertions.assertTrue(retryStrategy.retryRequest(request, new IOException(), 1, null)); } + @Test + void retryOnQueryRequests() { + final HttpQuery request = new HttpQuery("/"); + + Assertions.assertTrue(retryStrategy.retryRequest(request, new IOException(), 1, null)); + } + @Test void testRetryRequestWithResponseTimeoutAndRetryAfterHeader() { final HttpResponse response = new BasicHttpResponse(429, "Too Many Requests");