From 3f6158fc47ef9c023553387db68d6eedf411db9c Mon Sep 17 00:00:00 2001 From: Athou Date: Tue, 13 Oct 2026 06:34:07 +0200 Subject: [PATCH] add support for HTTP/2 (#2317) --- commafeed-server/pom.xml | 5 - .../commafeed/backend/HttpClientFactory.java | 73 +++--- .../com/commafeed/backend/HttpGetter.java | 219 +++++++++++------- .../service/PushNotificationService.java | 78 +++---- .../com/commafeed/backend/HttpGetterTest.java | 32 ++- 5 files changed, 236 insertions(+), 171 deletions(-) diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index 6c998751..e4751b71 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -456,11 +456,6 @@ ayza-for-apache5 10.1.0 - - org.brotli - dec - 0.1.2 - com.github.seancfoley ipaddress diff --git a/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java b/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java index ff6c88b1..70429c86 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java @@ -19,27 +19,27 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils; import org.apache.hc.client5.http.DnsResolver; import org.apache.hc.client5.http.SystemDefaultDnsResolver; +import org.apache.hc.client5.http.async.methods.InflatingAsyncDataConsumer; +import org.apache.hc.client5.http.async.methods.InflatingGzipDataConsumer; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.TlsConfig; -import org.apache.hc.client5.http.entity.DeflateInputStream; -import org.apache.hc.client5.http.entity.InputStreamFactory; import org.apache.hc.client5.http.entity.compress.ContentCoding; import org.apache.hc.client5.http.impl.DefaultRedirectStrategy; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; -import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; import org.apache.hc.client5.http.protocol.RedirectStrategy; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.nio.AsyncDataConsumer; import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; -import org.brotli.dec.BrotliInputStream; import java.net.InetAddress; import java.net.URI; @@ -48,7 +48,7 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.SequencedMap; -import java.util.zip.GZIPInputStream; +import java.util.function.UnaryOperator; @Singleton @RequiredArgsConstructor @@ -60,8 +60,8 @@ public class HttpClientFactory { private final CommaFeedConfiguration config; private final CommaFeedVersion version; - public CloseableHttpClient newClient(int poolSize) { - PoolingHttpClientConnectionManager connectionManager = + public CloseableHttpAsyncClient newClient(int poolSize) { + PoolingAsyncClientConnectionManager connectionManager = newConnectionManager(config, poolSize); String userAgent = config.httpClient() @@ -74,42 +74,46 @@ public class HttpClientFactory { return newClient(config, connectionManager, userAgent); } - private CloseableHttpClient newClient( + private CloseableHttpAsyncClient newClient( CommaFeedConfiguration config, - HttpClientConnectionManager connectionManager, + PoolingAsyncClientConnectionManager connectionManager, String userAgent) { List
headers = new ArrayList<>(); headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en")); headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache")); headers.add(new BasicHeader(HttpHeaders.CACHE_CONTROL, "no-cache")); - SequencedMap contentDecoderMap = new LinkedHashMap<>(); - contentDecoderMap.put(ContentCoding.GZIP.token(), GZIPInputStream::new); - contentDecoderMap.put(ContentCoding.DEFLATE.token(), DeflateInputStream::new); - contentDecoderMap.put(ContentCoding.BROTLI.token(), BrotliInputStream::new); + SequencedMap> contentDecoderMap = + new LinkedHashMap<>(); + contentDecoderMap.put( + ContentCoding.DEFLATE.token(), d -> new InflatingAsyncDataConsumer(d, null)); + contentDecoderMap.put(ContentCoding.GZIP.token(), InflatingGzipDataConsumer::new); RedirectStrategy redirectStrategy = config.httpClient().blockLocalAddresses() ? new BlockLocalAddressesRedirectStrategy(DNS_RESOLVER) : new DefaultRedirectStrategy(); - return HttpClientBuilder.create() - .disableConnectionState() - .useSystemProperties() - .disableAutomaticRetries() - .disableCookieManagement() - .setUserAgent(userAgent) - .setDefaultHeaders(headers) - .setConnectionManager(connectionManager) - .evictExpiredConnections() - .evictIdleConnections( - TimeValue.of(config.httpClient().idleConnectionsEvictionInterval())) - .setContentDecoderRegistry(new LinkedHashMap<>(contentDecoderMap)) - .setRedirectStrategy(redirectStrategy) - .build(); + CloseableHttpAsyncClient client = + HttpAsyncClients.custom() + .disableConnectionState() + .useSystemProperties() + .disableAutomaticRetries() + .disableCookieManagement() + .setUserAgent(userAgent) + .setDefaultHeaders(headers) + .setConnectionManager(connectionManager) + .evictExpiredConnections() + .evictIdleConnections( + TimeValue.of(config.httpClient().idleConnectionsEvictionInterval())) + .setContentDecoderMap(new LinkedHashMap<>(contentDecoderMap)) + .setRedirectStrategy(redirectStrategy) + .build(); + client.start(); + return client; } - private PoolingHttpClientConnectionManager newConnectionManager( + private PoolingAsyncClientConnectionManager newConnectionManager( CommaFeedConfiguration config, int poolSize) { SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build(); @@ -118,8 +122,8 @@ public class HttpClientFactory { ? new BlockLocalAddressesDnsResolver(DNS_RESOLVER) : DNS_RESOLVER; - return PoolingHttpClientConnectionManagerBuilder.create() - .setTlsSocketStrategy(Apache5SslUtils.toTlsSocketStrategy(sslFactory)) + return PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(Apache5SslUtils.toTlsStrategy(sslFactory)) .setDefaultConnectionConfig( ConnectionConfig.custom() .setConnectTimeout(Timeout.of(config.httpClient().connectTimeout())) @@ -129,6 +133,7 @@ public class HttpClientFactory { .build()) .setDefaultTlsConfig( TlsConfig.custom() + .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) .setHandshakeTimeout( Timeout.of(config.httpClient().sslHandshakeTimeout())) .build()) diff --git a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java index 9c44c626..6dd28f06 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java @@ -6,7 +6,6 @@ import com.commafeed.CommaFeedConfiguration.HttpClientCache; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Iterables; -import com.google.common.io.ByteStreams; import com.google.common.net.HttpHeaders; import jakarta.inject.Singleton; @@ -21,35 +20,44 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.async.methods.SimpleRequestProducer; import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.protocol.RedirectLocations; import org.apache.hc.client5.http.utils.DateUtils; -import org.apache.hc.core5.http.ClassicHttpRequest; -import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; import org.apache.hc.core5.http.NameValuePair; -import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.AsyncResponseConsumer; +import org.apache.hc.core5.http.nio.entity.AbstractBinAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; import org.apache.hc.core5.util.Timeout; import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.net.URI; +import java.nio.ByteBuffer; import java.time.Duration; import java.time.Instant; import java.time.InstantSource; import java.util.Optional; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; -/** Smart HTTP getter: handles gzip, ssl, last modified and etag headers */ +/** Smart HTTP getter: handles gzip, ssl, last modified, and etag headers */ @Singleton @Slf4j public class HttpGetter { private final CommaFeedConfiguration config; private final InstantSource instantSource; - private final CloseableHttpClient client; + private final CloseableHttpAsyncClient client; private final Cache cache; public HttpGetter( @@ -157,63 +165,83 @@ public class HttpGetter { .setProtocolUpgradeEnabled(false) .build()); - return client.execute( - request.toClassicHttpRequest(), - context, - resp -> { - byte[] content = - resp.getEntity() == null - ? null - : toByteArray( - resp.getEntity(), - config.httpClient().maxResponseSize().asLongValue()); - int code = resp.getCode(); - String lastModifiedHeader = - Optional.ofNullable(resp.getFirstHeader(HttpHeaders.LAST_MODIFIED)) - .map(NameValuePair::getValue) - .map(StringUtils::trimToNull) - .orElse(null); - String eTagHeader = - Optional.ofNullable(resp.getFirstHeader(HttpHeaders.ETAG)) - .map(NameValuePair::getValue) - .map(StringUtils::trimToNull) - .orElse(null); + AsyncRequestProducer req = SimpleRequestProducer.create(request.toSimpleHttpRequest()); + AsyncResponseConsumer> resp = + new BasicResponseConsumer<>( + new LimitedBytesEntityConsumer( + config.httpClient().maxResponseSize().asLongValue())); + FutureCallback> callback = + new FutureCallback<>() { + @Override + public void completed( + Message result) {} - CacheControl cacheControl = - Optional.ofNullable(resp.getFirstHeader(HttpHeaders.CACHE_CONTROL)) - .map(NameValuePair::getValue) - .map(StringUtils::trimToNull) - .map(HttpGetter::toCacheControl) - .orElse(null); + @Override + public void failed(Exception ex) {} - Instant retryAfter = - Optional.ofNullable(resp.getFirstHeader(HttpHeaders.RETRY_AFTER)) - .map(NameValuePair::getValue) - .map(StringUtils::trimToNull) - .map(this::toInstant) - .orElse(null); + @Override + public void cancelled() {} + }; + Future> future = + client.execute(req, resp, context, callback); - String contentType = - Optional.ofNullable(resp.getEntity()) - .map(HttpEntity::getContentType) - .orElse(null); - String urlAfterRedirect = - Optional.ofNullable(context.getRedirectLocations()) - .map(RedirectLocations::getAll) - .map(l -> Iterables.getLast(l, null)) - .map(URI::toString) - .orElse(request.getUrl()); + try { + Message message = future.get(); - return new HttpResponse( - code, - lastModifiedHeader, - eTagHeader, - cacheControl, - retryAfter, - content, - contentType, - urlAfterRedirect); - }); + byte[] content = message.getBody(); + int code = message.getHead().getCode(); + String lastModifiedHeader = + Optional.ofNullable(message.getHead().getFirstHeader(HttpHeaders.LAST_MODIFIED)) + .map(NameValuePair::getValue) + .map(StringUtils::trimToNull) + .orElse(null); + String eTagHeader = + Optional.ofNullable(message.getHead().getFirstHeader(HttpHeaders.ETAG)) + .map(NameValuePair::getValue) + .map(StringUtils::trimToNull) + .orElse(null); + + CacheControl cacheControl = + Optional.ofNullable(message.getHead().getFirstHeader(HttpHeaders.CACHE_CONTROL)) + .map(NameValuePair::getValue) + .map(StringUtils::trimToNull) + .map(HttpGetter::toCacheControl) + .orElse(null); + + Instant retryAfter = + Optional.ofNullable(message.getHead().getFirstHeader(HttpHeaders.RETRY_AFTER)) + .map(NameValuePair::getValue) + .map(StringUtils::trimToNull) + .map(this::toInstant) + .orElse(null); + + String contentType = + Optional.ofNullable(message.getHead().getFirstHeader(HttpHeaders.CONTENT_TYPE)) + .map(NameValuePair::getValue) + .map(StringUtils::trimToNull) + .orElse(null); + String urlAfterRedirect = + Optional.ofNullable(context.getRedirectLocations()) + .map(RedirectLocations::getAll) + .map(l -> Iterables.getLast(l, null)) + .map(URI::toString) + .orElse(request.getUrl()); + + return new HttpResponse( + code, + lastModifiedHeader, + eTagHeader, + cacheControl, + retryAfter, + content, + contentType, + urlAfterRedirect); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("interrupted while fetching " + request.getUrl(), e); + } catch (ExecutionException e) { + throw Lombok.sneakyThrow(e.getCause()); + } } private static CacheControl toCacheControl(String headerValue) { @@ -237,28 +265,6 @@ public class HttpGetter { return DateUtils.parseStandardDate(headerValue); } - private static byte[] toByteArray(HttpEntity entity, long maxBytes) throws IOException { - if (entity.getContentLength() > maxBytes) { - throw new IOException( - "Response size (%s bytes) exceeds the maximum allowed size (%s bytes)" - .formatted(entity.getContentLength(), maxBytes)); - } - - try (InputStream input = entity.getContent()) { - if (input == null) { - return null; - } - - byte[] bytes = ByteStreams.limit(input, maxBytes + 1).readAllBytes(); - if (bytes.length > maxBytes) { - throw new IOException( - "Response size exceeds the maximum allowed size (%s bytes)" - .formatted(maxBytes)); - } - return bytes; - } - } - private static Cache newCache(CommaFeedConfiguration config) { HttpClientCache cacheConfig = config.httpClient().cache(); if (!cacheConfig.enabled()) { @@ -274,6 +280,43 @@ public class HttpGetter { .build(); } + @RequiredArgsConstructor + private static class LimitedBytesEntityConsumer extends AbstractBinAsyncEntityConsumer { + + private final long maxBytes; + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + @Override + protected void streamStart(ContentType contentType) {} + + @Override + protected int capacityIncrement() { + return Integer.MAX_VALUE; + } + + @Override + protected void data(ByteBuffer src, boolean endOfStream) throws IOException { + if (out.size() + src.remaining() > maxBytes) { + throw new IOException( + "Response size exceeds the maximum allowed size (%s bytes)" + .formatted(maxBytes)); + } + byte[] chunk = new byte[src.remaining()]; + src.get(chunk); + out.write(chunk, 0, chunk.length); + } + + @Override + protected byte[] generateContent() { + return out.toByteArray(); + } + + @Override + public void releaseResources() { + out.reset(); + } + } + public static class SchemeNotAllowedException extends Exception { private static final long serialVersionUID = 1L; @@ -336,15 +379,15 @@ public class HttpGetter { return new HttpRequestBuilder().url(url); } - public ClassicHttpRequest toClassicHttpRequest() { - ClassicHttpRequest req = ClassicRequestBuilder.get(url).build(); + public SimpleHttpRequest toSimpleHttpRequest() { + SimpleRequestBuilder builder = SimpleRequestBuilder.get(url); if (lastModified != null) { - req.addHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified); + builder.addHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified); } if (eTag != null) { - req.addHeader(HttpHeaders.IF_NONE_MATCH, eTag); + builder.addHeader(HttpHeaders.IF_NONE_MATCH, eTag); } - return req; + return builder.build(); } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/PushNotificationService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/PushNotificationService.java index e91fcaf4..0c2cbd92 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/PushNotificationService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/PushNotificationService.java @@ -16,26 +16,28 @@ import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.NameValuePair; -import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicNameValuePair; +import org.apache.hc.core5.net.WWWFormCodec; import org.apache.hc.core5.util.Timeout; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; @Singleton @Slf4j public class PushNotificationService { - private final CloseableHttpClient httpClient; + private final CloseableHttpAsyncClient httpClient; private final Meter meter; private final CommaFeedConfiguration config; @@ -97,13 +99,13 @@ public class PushNotificationService { return; } - HttpPost request = new HttpPost(serverUrl + "/" + topic); - request.setConfig( + SimpleRequestBuilder request = SimpleRequestBuilder.post(serverUrl + "/" + topic); + request.setRequestConfig( RequestConfig.custom() .setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())) .build()); request.addHeader("Title", feedTitle); - request.setEntity(new StringEntity(entryTitle, StandardCharsets.UTF_8)); + request.setBody(entryTitle, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); if (StringUtils.isNotBlank(entryUrl)) { request.addHeader("Click", entryUrl); @@ -113,15 +115,7 @@ public class PushNotificationService { request.addHeader("Authorization", "Bearer " + settings.getUserSecret()); } - httpClient.execute( - request, - response -> { - if (response.getCode() >= 400) { - throw new PushNotificationException( - "ntfy notification failed with status " + response.getCode()); - } - return null; - }); + send("ntfy", request.build()); } private void sendGotify( @@ -152,23 +146,15 @@ public class PushNotificationService { .put("click", new JsonObject().put("url", entryUrl)))); } - HttpPost request = new HttpPost(serverUrl + "/message"); - request.setConfig( + SimpleRequestBuilder request = SimpleRequestBuilder.post(serverUrl + "/message"); + request.setRequestConfig( RequestConfig.custom() .setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())) .build()); request.addHeader("X-Gotify-Key", token); - request.setEntity(new StringEntity(json.toString(), ContentType.APPLICATION_JSON)); + request.setBody(json.toString(), ContentType.APPLICATION_JSON); - httpClient.execute( - request, - response -> { - if (response.getCode() >= 400) { - throw new PushNotificationException( - "gotify notification failed with status " + response.getCode()); - } - return null; - }); + send("gotify", request.build()); } private void sendPushover( @@ -194,22 +180,32 @@ public class PushNotificationService { params.add(new BasicNameValuePair("url", entryUrl)); } - HttpPost request = new HttpPost("https://api.pushover.net/1/messages.json"); - request.setConfig( + SimpleRequestBuilder request = + SimpleRequestBuilder.post("https://api.pushover.net/1/messages.json"); + request.setRequestConfig( RequestConfig.custom() .setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())) .build()); - request.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8)); + request.setBody( + WWWFormCodec.format(params, StandardCharsets.UTF_8), + ContentType.APPLICATION_FORM_URLENCODED.withCharset(StandardCharsets.UTF_8)); - httpClient.execute( - request, - response -> { - if (response.getCode() >= 400) { - throw new PushNotificationException( - "pushover notification failed with status " + response.getCode()); - } - return null; - }); + send("pushover", request.build()); + } + + private void send(String name, SimpleHttpRequest request) throws IOException { + try { + SimpleHttpResponse response = httpClient.execute(request, null).get(); + if (response.getCode() >= 400) { + throw new PushNotificationException( + name + " notification failed with status " + response.getCode()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("interrupted while sending " + name + " notification", e); + } catch (ExecutionException e) { + throw new IOException(e.getCause()); + } } public static class PushNotificationException extends RuntimeException { diff --git a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java index 22126aba..d8534d82 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java @@ -29,6 +29,7 @@ import org.mockserver.model.Delay; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; import org.mockserver.model.MediaType; +import org.mockserver.model.Protocol; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -359,8 +360,7 @@ class HttpGetterTest { IOException e = Assertions.assertThrows(IOException.class, () -> getter.get(this.feedUrl)); Assertions.assertEquals( - "Response size (100000 bytes) exceeds the maximum allowed size (10000 bytes)", - e.getMessage()); + "Response size exceeds the maximum allowed size (10000 bytes)", e.getMessage()); } @Test @@ -416,7 +416,7 @@ class HttpGetterTest { @Nested class Compression { - private static final String ACCEPT_ENCODING = "gzip, deflate, br"; + private static final String ACCEPT_ENCODING = "deflate, gzip"; @Test void gzip() throws Exception { @@ -590,4 +590,30 @@ class HttpGetterTest { UnknownHostException.class, () -> getter.get("http://[::a00:1]")); // 10.0.0.1 } } + + @Nested + class Protocols { + @Test + void usesHttp2OverTls() throws Exception { + mockServerClient + .when(HttpRequest.request().withMethod("GET").withProtocol(Protocol.HTTP_2)) + .respond(HttpResponse.response().withBody("h2")); + mockServerClient + .when(HttpRequest.request().withMethod("GET").withProtocol(Protocol.HTTP_1_1)) + .respond(HttpResponse.response().withBody("h1")); + + HttpResult result = getter.get("https://localhost:" + mockServerClient.getPort()); + Assertions.assertEquals("h2", new String(result.content())); + } + + @Test + void fallsBackToHttp1OverCleartext() throws Exception { + mockServerClient + .when(HttpRequest.request().withMethod("GET").withProtocol(Protocol.HTTP_1_1)) + .respond(HttpResponse.response().withBody("h1")); + + HttpResult result = getter.get("http://localhost:" + mockServerClient.getPort()); + Assertions.assertEquals("h1", new String(result.content())); + } + } }