http2apache-httpclient-5.x

How to handle HTTP/2 using Apache CloseableHttpClient


If I send the following command to the server for HTTP/2 connection testing, it works well.

 curl -v --http2  http://192.168.0.171:20002 
*   Trying 192.168.0.171:20002...
* Connected to 192.168.0.171 (192.168.0.171) port 20002
> GET / HTTP/1.1
> Host: 192.168.0.171:20002
> User-Agent: curl/8.7.1
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAAQAoAAAAAIAAAAA
> 
* Request completely sent off
< HTTP/1.1 101 
< Connection: Upgrade
< Upgrade: h2c
< Date: Mon, 02 Dec 2024 07:08:28 GMT
< 
* Received 101, Switching to HTTP/2
< HTTP/2 200 
< content-type: text/plain;charset=UTF-8
< content-length: 13
< date: Mon, 02 Dec 2024 07:08:28 GMT
< 
* Connection #0 to host 192.168.0.171 left intact
OnlineTestApp%                                                  

However, when running the following code, the following error occurs.

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultClientConnectionReuseStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory;
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.ManagedHttpClientConnection;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.URIScheme;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.HttpConnectionFactory;
import org.apache.hc.core5.http.ssl.TLS;
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
import org.apache.hc.core5.pool.PoolReusePolicy;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.ssl.TrustStrategy;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
 * https://stackoverflow.com/questions/50294297/async-response-streaming-with-apache-async-http-client
 * https://stackoverflow.com/questions/77339910/how-do-i-get-inputstream-from-httpclient5-response
 * https://github.com/apache/httpcomponents-client/blob/master/httpclient5/src/test/java/org/apache/hc/client5/http/examples/ClientConfiguration.java
 */
public class ClosableHttpClientTest {

    @Test
    public void test() {

        CloseableHttpClient client = createClient(false);

        RequestConfig config = RequestConfig.custom()
                .setConnectionRequestTimeout(3, TimeUnit.SECONDS)
                .setResponseTimeout(3, TimeUnit.SECONDS)
                .build();


        HttpGet httpGet = new HttpGet("http://192.168.0.171:20002");
        httpGet.setVersion(HttpVersion.HTTP_2_0);
        httpGet.setConfig(config);

        try {
            ClassicHttpResponse resp = client.executeOpen(null, httpGet, null);
            try (InputStream in = resp.getEntity().getContent()) {
                byte[] buf = in.readAllBytes();
                System.err.println(new String(buf));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @BeforeAll
    static void beforeAll() {

    }

    private CloseableHttpClient createClient(boolean http2) {

        SSLConnectionSocketFactory sslsf = getTrustAllConnectionSocketFactory();

        int connTimeout = 3;
        ConnectionConfig connConfig = ConnectionConfig.custom()
                .setConnectTimeout(Timeout.of(connTimeout, TimeUnit.SECONDS))
                .build();

        PoolingHttpClientConnectionManager connManager = PoolingHttpClientConnectionManagerBuilder.create()
                .setSSLSocketFactory(sslsf)
                .setMaxConnTotal(200)
                .setMaxConnPerRoute(100)
                .setDefaultConnectionConfig(connConfig)
                .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.LAX)
                .setConnPoolPolicy(PoolReusePolicy.LIFO)
                .build();
                ;

        CloseableHttpClient client = HttpClients.custom()
                .setUserAgent("TEST_AGENT")
                .setConnectionManager(connManager)
                .setConnectionReuseStrategy(DefaultClientConnectionReuseStrategy.INSTANCE)
                .disableAuthCaching()
                .disableRedirectHandling()
                .disableConnectionState()
                .disableCookieManagement()
                .evictExpiredConnections()
                .evictIdleConnections(TimeValue.of(10, TimeUnit.SECONDS))
                .build();

        int reqTimeout = 30; // 처리 프로세스는 10초 응답 처리.
        return client;
    }


    private static SSLConnectionSocketFactory getTrustAllConnectionSocketFactory() {
        SSLConnectionSocketFactory trustAllConnectionSocketFactory = null;
        try {
            final TrustStrategy acceptingTrustStrategy = (chain, authType) -> {
                // if (LOG.isDebugEnabled()) {
                //     int i = 0;
                //     for (X509Certificate cert : chain) {
                //         LOG.debug("HostnameVerifier.isTrusted chain[{}] {}", i, cert);
                //         i++;
                //     }
                //     LOG.debug("HostnameVerifier.isTrusted authType: {}", authType);
                // }
                return true;
            };

            final SSLContext sslContext = SSLContexts.custom()
                    .loadTrustMaterial(null, acceptingTrustStrategy)
                    .build();

            trustAllConnectionSocketFactory = SSLConnectionSocketFactoryBuilder.create()
                    .setSslContext(sslContext)
                    .setHostnameVerifier((s, sslSession) -> {
                        // if (LOG.isDebugEnabled()) {
                        // LOG.debug("HostnameVerifier.verify {} {}", s, sslSession);
                        // }
                        return true;
                    })
                    .setTlsVersions(TLS.V_1_3, TLS.V_1_2, TLS.V_1_1, TLS.V_1_0)
                    .build();

        } catch (Exception ex) {
            // LOG.error("failed to create trustAll SSLConnectionSocketFactory - {}", ex.getMessage(), ex);
            trustAllConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory();
            // LOG.error("use default SSLConnectionSocketFactory - {}", trustAllConnectionSocketFactory.getClass().getName());
        }
        return trustAllConnectionSocketFactory;
    }
}

16:05:01.069 [main] DEBUG org.apache.hc.client5.http.headers -- http-outgoing-0 >> GET / HTTP/2.0
16:05:01.069 [main] DEBUG org.apache.hc.client5.http.headers -- http-outgoing-0 >> Accept-Encoding: gzip, x-gzip, deflate
16:05:01.069 [main] DEBUG org.apache.hc.client5.http.headers -- http-outgoing-0 >> Host: 192.168.0.171:20002
16:05:01.069 [main] DEBUG org.apache.hc.client5.http.headers -- http-outgoing-0 >> Connection: keep-alive
16:05:01.069 [main] DEBUG org.apache.hc.client5.http.headers -- http-outgoing-0 >> User-Agent: Herb/3.0
16:05:01.069 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 >> "GET / HTTP/2.0[\r][\n]"
16:05:01.069 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 >> "Accept-Encoding: gzip, x-gzip, deflate[\r][\n]"
16:05:01.069 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 >> "Host: 192.168.0.171:20002[\r][\n]"
16:05:01.069 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 >> "Connection: keep-alive[\r][\n]"
16:05:01.069 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 >> "User-Agent: Herb/3.0[\r][\n]"
16:05:01.069 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 >> "[\r][\n]"
16:05:01.083 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 << "HTTP/1.1 505 [\r][\n]"
16:05:01.083 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 << "Content-Type: text/html;charset=utf-8[\r][\n]"
16:05:01.083 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 << "Content-Language: en[\r][\n]"
16:05:01.083 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 << "Content-Length: 465[\r][\n]"
16:05:01.083 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 << "Date: Mon, 02 Dec 2024 07:02:05 GMT[\r][\n]"
16:05:01.083 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 << "[\r][\n]"
16:05:01.083 [main] DEBUG org.apache.hc.client5.http.wire -- http-outgoing-0 << "<!doctype html><html lang="en"><head><title>HTTP Status 505 [0xffffffe2][0xffffff80][0xffffff93] HTTP Version Not Supported</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 505 [0xffffffe2][0xffffff80][0xffffff93] HTTP Version Not Supported</h1></body></html>"
16:05:01.085 [main] DEBUG org.apache.hc.client5.http.headers -- http-outgoing-0 << HTTP/1.1 505 
16:05:01.085 [main] DEBUG org.apache.hc.client5.http.headers -- http-outgoing-0 << Content-Type: text/html;charset=utf-8
16:05:01.085 [main] DEBUG org.apache.hc.client5.http.headers -- http-outgoing-0 << Content-Language: en
16:05:01.085 [main] DEBUG org.apache.hc.client5.http.headers -- http-outgoing-0 << Content-Length: 465
16:05:01.085 [main] DEBUG org.apache.hc.client5.http.headers -- http-outgoing-0 << Date: Mon, 02 Dec 2024 07:02:05 GMT
16:05:01.087 [main] DEBUG org.apache.hc.client5.http.impl.classic.MainClientExec -- ex-0000000001 connection can be kept alive for 3 MINUTES
16:05:01.088 [main] DEBUG org.apache.hc.client5.http.impl.classic.InternalHttpClient -- ep-0000000001 releasing valid endpoint
16:05:01.088 [main] DEBUG org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager -- ep-0000000001 releasing endpoint
16:05:01.088 [main] DEBUG org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager -- ep-0000000001 connection http-outgoing-0 can be kept alive for 3 MINUTES
16:05:01.088 [main] DEBUG org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager -- ep-0000000001 connection released [route: {}->http://192.168.0.171:20002][total available: 1; route allocated: 1 of 100; total allocated: 1 of 100]
<!doctype html><html lang="en"><head><title>HTTP Status 505 – HTTP Version Not Supported</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 505 – HTTP Version Not Supported</h1></body></html>

The development dependencies are as follows

// apache http client
implementation 'org.apache.httpcomponents.client5:httpclient5:5.3.1'

The reason for using CloseableHttpClient is that when receiving large amounts of data, CloseableAsyncHttpClient returns an InputStream, making stream processing impossible. Therefore, I need to use CloseableHttpClient, but the problem is that HTTP/2 requests result in errors. While Java 11 HttpClient could be an alternative, it cannot be used due to internal circumstances. I need help.

Handling HTTP/2 requests using CloseableHttpClient


Solution

    1. HTTP protocol level gets negotiated on the per connection basis. The protocol version at the message level for outgoing messages is merely a hint, nothing more. A message with a HTTP/1.0 hint can be transmitted over HTTP/1.1 using HTTP/1.0 compatible semantics. HTTP/2.0 hint over HTTP/1.1 connection makes no sense.

    2. Classic HttpClient supports HTTP/1.1 protocol only. One must use async version of HttpClient in order to get HTTP/2.0 support. Usually HTTP/2 will be automatically negotiated whenever possible. One, however, can force HttpCclient to use HTTP/2 only.

    3. HttpClient presently does not support h2c upgrade over HTTP/1.1. One must either use TLS and have HTTP/2 negotated or force HTTP/2 over plain connections

    final PoolingAsyncClientConnectionManager cm = PoolingAsyncClientConnectionManagerBuilder.create()
            .setDefaultTlsConfig(TlsConfig.custom()
                    .setVersionPolicy(HttpVersionPolicy.NEGOTIATE)
                    .build())
            .build();
    try (final CloseableHttpAsyncClient client = HttpAsyncClients.custom()
            .setConnectionManager(cm)
            .build()) {
    
        client.start();
    
        final SimpleHttpRequest request = SimpleRequestBuilder.get()
                .setHttpHost(new HttpHost("httpbin.org"))
                .setPath("/headers")
                .build();
    
        System.out.println("Executing request " + request);
        final Future<SimpleHttpResponse> future = client.execute(
                SimpleRequestProducer.create(request),
                SimpleResponseConsumer.create(),
                new FutureCallback<SimpleHttpResponse>() {
    
                    @Override
                    public void completed(final SimpleHttpResponse response) {
                        System.out.println(request + "->" + new StatusLine(response));
                        System.out.println(response.getBody());
                    }
    
                    @Override
                    public void failed(final Exception ex) {
                        System.out.println(request + "->" + ex);
                    }
    
                    @Override
                    public void cancelled() {
                        System.out.println(request + " cancelled");
                    }
    
                });
        future.get();
    
        System.out.println("Shutting down");
        client.close(CloseMode.GRACEFUL);
    }