From 713c0fce7c7c836505714052221d36014a69411b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 12 Apr 2021 07:44:11 +0100 Subject: [PATCH] Auto-configure H2C when HTTP/2 is enabled and SSL is disabled --- .../src/docs/asciidoc/howto.adoc | 85 +++---------------- spring-boot-project/spring-boot/build.gradle | 1 + .../jetty/JettyReactiveWebServerFactory.java | 26 +++--- .../jetty/JettyServletWebServerFactory.java | 18 ++-- .../netty/NettyReactiveWebServerFactory.java | 15 +++- .../TomcatReactiveWebServerFactory.java | 8 +- .../tomcat/TomcatServletWebServerFactory.java | 6 +- .../UndertowWebServerFactoryDelegate.java | 10 +-- ...AbstractReactiveWebServerFactoryTests.java | 43 ++++++++++ .../AbstractServletWebServerFactoryTests.java | 41 +++++++++ 10 files changed, 148 insertions(+), 105 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc index ee73c11823c1..3253a7184dc2 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc @@ -594,26 +594,23 @@ We recommend using `application.properties` to configure HTTPS, as the HTTP conn [[howto-configure-http2]] === Configure HTTP/2 You can enable HTTP/2 support in your Spring Boot application with the configprop:server.http2.enabled[] configuration property. -This support depends on the chosen web server and the application environment, since that protocol is not supported out-of-the-box by all JDK8 releases. +Both `h2` (HTTP/2 over TLS) and `h2c` (HTTP/2 over TCP) are supported. +To use `h2`, SSL must also be enabled. +When SSL is not enabled, `h2c` will be used. +The details of the `h2` support depend on the chosen web server and the application environment, since that protocol is not supported out-of-the-box by all JDK 8 releases. -[NOTE] -==== -Spring Boot does not advise using `h2c`, the cleartext version of the HTTP/2 protocol. -As a result, the following sections require you to <>. -If you still choose to use `h2c`, you can check <>. -==== [[howto-configure-http2-tomcat]] ==== HTTP/2 with Tomcat -Spring Boot ships by default with Tomcat 9.0.x which supports HTTP/2 out of the box when using JDK 9 or later. -Alternatively, HTTP/2 can be used on JDK 8 if the `libtcnative` library and its dependencies are installed on the host operating system. +Spring Boot ships by default with Tomcat 9.0.x which supports `h2c` out of the box and `h2` out of the box when using JDK 9 or later. +Alternatively, `h2` can be used on JDK 8 if the `libtcnative` library and its dependencies are installed on the host operating system. The library directory must be made available, if not already, to the JVM library path. You can do so with a JVM argument such as `-Djava.library.path=/usr/local/opt/tomcat-native/lib`. More on this in the https://tomcat.apache.org/tomcat-9.0-doc/apr.html[official Tomcat documentation]. -Starting Tomcat 9.0.x on JDK 8 without that native support logs the following error: +Starting Tomcat 9.0.x on JDK 8 with HTTP/2 and SSL enabled but without that native support logs the following error: [indent=0,subs="attributes"] ---- @@ -626,7 +623,8 @@ This error is not fatal, and the application still starts with HTTP/1.1 SSL supp [[howto-configure-http2-jetty]] ==== HTTP/2 with Jetty For HTTP/2 support, Jetty requires the additional `org.eclipse.jetty.http2:http2-server` dependency. -Now depending on your deployment, you also need to choose other dependencies: +To use `h2c` no other dependencies are required. +To use `h2`, you also need to choose one of the following dependencies, depending on your deployment: * `org.eclipse.jetty:jetty-alpn-java-server` for applications running on JDK9+ * `org.eclipse.jetty:jetty-alpn-openjdk8-server` for applications running on JDK8u252+ @@ -637,8 +635,9 @@ Now depending on your deployment, you also need to choose other dependencies: [[howto-configure-http2-netty]] ==== HTTP/2 with Reactor Netty The `spring-boot-webflux-starter` is using by default Reactor Netty as a server. -Reactor Netty can be configured for HTTP/2 using the JDK support with JDK 9 or later. -For JDK 8 environments, or for optimal runtime performance, this server also supports HTTP/2 with native libraries. +Reactor Netty supports `h2c` using JDK 8 or later with no additional dependencies. +Reactor Netty supports `h2` using the JDK support with JDK 9 or later. +For JDK 8 environments, or for optimal runtime performance, this server also supports `h2` with native libraries. To enable that, your application needs to have an additional dependency. Spring Boot manages the version for the `io.netty:netty-tcnative-boringssl-static` "uber jar", containing native libraries for all platforms. @@ -648,65 +647,7 @@ Developers can choose to import only the required dependencies using a classifie [[howto-configure-http2-undertow]] ==== HTTP/2 with Undertow -As of Undertow 1.4.0+, HTTP/2 is supported without any additional requirement on JDK8. - - - -[[howto-configure-http2-h2c]] -==== HTTP/2 Cleartext with supported servers -To enable HTTP/2 with cleartext support, you need to leave the configprop:server.http2.enabled[] property set to `false`, -and instead apply a customizer specific to your choice of server: - -For Tomcat, we need to add an upgrade protocol: - -[source,java,pending-extract=true,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public TomcatConnectorCustomizer connectorCustomizer() { - return (connector) -> connector.addUpgradeProtocol(new Http2Protocol()); - } ----- - -For Jetty, we need to add a connection factory to the existing connector: - -[source,java,pending-extract=true,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public JettyServerCustomizer serverCustomizer() { - return (server) -> { - HttpConfiguration configuration = new HttpConfiguration(); - configuration.setSendServerVersion(false); - Arrays.stream(server.getConnectors()) - .filter(connector -> connector instanceof ServerConnector) - .map(ServerConnector.class::cast) - .forEach(connector -> { - connector.addConnectionFactory(new HTTP2CServerConnectionFactory(configuration)); - }); - }; - } ----- - -For Netty, we need to add `h2c` as a supported protocol: - -[source,java,pending-extract=true,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public NettyServerCustomizer serverCustomizer() { - return (server) -> server.protocol(HttpProtocol.H2C); - } ----- - -For Undertow, we need to enable the HTTP2 option: - -[source,java,pending-extract=true,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public UndertowBuilderCustomizer builderCustomizer() { - return (builder) -> { - builder.setServerOption(ENABLE_HTTP2, true); - }; - } ----- +As of Undertow 1.4.0+, both `h2` and `h2c` are supported on JDK 8 without any additional dependencies. diff --git a/spring-boot-project/spring-boot/build.gradle b/spring-boot-project/spring-boot/build.gradle index 3b553dbcc34c..ad71998ea549 100644 --- a/spring-boot-project/spring-boot/build.gradle +++ b/spring-boot-project/spring-boot/build.gradle @@ -47,6 +47,7 @@ dependencies { optional("junit:junit") optional("org.apache.commons:commons-dbcp2") optional("org.apache.httpcomponents:httpclient") + optional("org.apache.httpcomponents.client5:httpclient5") optional("org.apache.logging.log4j:log4j-api") optional("org.apache.logging.log4j:log4j-core") optional("org.apache.tomcat.embed:tomcat-embed-core") diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java index fb195d6c55fc..3913bd9f3082 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,16 @@ package org.springframework.boot.web.embedded.jetty; import java.net.InetSocketAddress; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Handler; @@ -193,23 +196,26 @@ protected Server createJettyServer(JettyHttpHandlerAdapter servlet) { } private AbstractConnector createConnector(InetSocketAddress address, Server server) { - ServerConnector connector; + HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.setSendServerVersion(false); + List connectionFactories = new ArrayList<>(); + if (getHttp2() != null && getHttp2().isEnabled()) { + connectionFactories.add(new HTTP2ServerConnectionFactory(httpConfiguration)); + } + connectionFactories.add(new HttpConnectionFactory(httpConfiguration)); JettyResourceFactory resourceFactory = getResourceFactory(); + ServerConnector connector; if (resourceFactory != null) { connector = new ServerConnector(server, resourceFactory.getExecutor(), resourceFactory.getScheduler(), - resourceFactory.getByteBufferPool(), this.acceptors, this.selectors, new HttpConnectionFactory()); + resourceFactory.getByteBufferPool(), this.acceptors, this.selectors, + connectionFactories.toArray(new ConnectionFactory[0])); } else { - connector = new ServerConnector(server, this.acceptors, this.selectors); + connector = new ServerConnector(server, this.acceptors, this.selectors, + connectionFactories.toArray(new ConnectionFactory[0])); } connector.setHost(address.getHostString()); connector.setPort(address.getPort()); - for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) { - if (connectionFactory instanceof HttpConfiguration.ConnectionFactory) { - ((HttpConfiguration.ConnectionFactory) connectionFactory).getHttpConfiguration() - .setSendServerVersion(false); - } - } return connector; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index afb02094f687..5495d81508cf 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -34,11 +34,13 @@ import java.util.Set; import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.ErrorHandler; @@ -175,15 +177,17 @@ private Server createServer(InetSocketAddress address) { } private AbstractConnector createConnector(InetSocketAddress address, Server server) { - ServerConnector connector = new ServerConnector(server, this.acceptors, this.selectors); + HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.setSendServerVersion(false); + List connectionFactories = new ArrayList<>(); + if (getHttp2() != null && getHttp2().isEnabled()) { + connectionFactories.add(new HTTP2ServerConnectionFactory(httpConfiguration)); + } + connectionFactories.add(new HttpConnectionFactory(httpConfiguration)); + ServerConnector connector = new ServerConnector(server, this.acceptors, this.selectors, + connectionFactories.toArray(new ConnectionFactory[0])); connector.setHost(address.getHostString()); connector.setPort(address.getPort()); - for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) { - if (connectionFactory instanceof HttpConfiguration.ConnectionFactory) { - ((HttpConfiguration.ConnectionFactory) connectionFactory).getHttpConfiguration() - .setSendServerVersion(false); - } - } return connector; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java index 7bf0bdf14c6b..ba797507dadc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -183,10 +183,17 @@ private HttpServer customizeSslConfiguration(HttpServer httpServer) { } private HttpProtocol[] listProtocols() { - if (getHttp2() != null && getHttp2().isEnabled() && getSsl() != null && getSsl().isEnabled()) { - return new HttpProtocol[] { HttpProtocol.H2, HttpProtocol.HTTP11 }; + List protocols = new ArrayList<>(); + protocols.add(HttpProtocol.HTTP11); + if (getHttp2() != null && getHttp2().isEnabled()) { + if (getSsl() != null && getSsl().isEnabled()) { + protocols.add(HttpProtocol.H2); + } + else { + protocols.add(HttpProtocol.H2C); + } } - return new HttpProtocol[] { HttpProtocol.HTTP11 }; + return protocols.toArray(new HttpProtocol[0]); } private InetSocketAddress getListenAddress() { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java index 7ce213cba849..8696ac6f82fb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -190,6 +190,9 @@ protected void customizeConnector(Connector connector) { } // Don't bind to the socket prematurely if ApplicationContext is slow to start connector.setProperty("bindOnInit", "false"); + if (getHttp2() != null && getHttp2().isEnabled()) { + connector.addUpgradeProtocol(new Http2Protocol()); + } if (getSsl() != null && getSsl().isEnabled()) { customizeSsl(connector); } @@ -214,9 +217,6 @@ private void customizeProtocol(AbstractProtocol protocol) { private void customizeSsl(Connector connector) { new SslConnectorCustomizer(getSsl(), getSslStoreProvider()).customize(connector); - if (getHttp2() != null && getHttp2().isEnabled()) { - connector.addUpgradeProtocol(new Http2Protocol()); - } } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index 84a099284234..35b3b7383a43 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -322,6 +322,9 @@ protected void customizeConnector(Connector connector) { } // Don't bind to the socket prematurely if ApplicationContext is slow to start connector.setProperty("bindOnInit", "false"); + if (getHttp2() != null && getHttp2().isEnabled()) { + connector.addUpgradeProtocol(new Http2Protocol()); + } if (getSsl() != null && getSsl().isEnabled()) { customizeSsl(connector); } @@ -346,9 +349,6 @@ private void invokeProtocolHandlerCustomizers(ProtocolHandler protocolHandler) { private void customizeSsl(Connector connector) { new SslConnectorCustomizer(getSsl(), getSslStoreProvider()).customize(connector); - if (getHttp2() != null && getHttp2().isEnabled()) { - connector.addUpgradeProtocol(new Http2Protocol()); - } } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java index 925df1b00c18..0b17e7a908c3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -158,12 +158,12 @@ Builder createBuilder(AbstractConfigurableWebServerFactory factory) { if (this.directBuffers != null) { builder.setDirectBuffers(this.directBuffers); } + Http2 http2 = factory.getHttp2(); + if (http2 != null) { + builder.setServerOption(UndertowOptions.ENABLE_HTTP2, http2.isEnabled()); + } if (ssl != null && ssl.isEnabled()) { new SslBuilderCustomizer(factory.getPort(), address, ssl, factory.getSslStoreProvider()).customize(builder); - Http2 http2 = factory.getHttp2(); - if (http2 != null) { - builder.setServerOption(UndertowOptions.ENABLE_HTTP2, http2.isEnabled()); - } } else { builder.addHttpListener(port, (address != null) ? address.getHostAddress() : "0.0.0.0"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java index 859f7fba4ede..252fe8c665a5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.web.reactive.server; import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; @@ -28,6 +29,7 @@ import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -39,6 +41,13 @@ import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequests; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.ContentType; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; @@ -52,6 +61,7 @@ import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.GracefulShutdownResult; +import org.springframework.boot.web.server.Http2; import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServer; @@ -448,6 +458,39 @@ void whenARequestIsActiveThenStopWillComplete() throws InterruptedException, Bro blockingHandler.completeOne(); } + @Test + void whenHttp2IsEnabledAndSslIsDisabledThenH2cCanBeUsed() + throws InterruptedException, ExecutionException, IOException { + AbstractReactiveWebServerFactory factory = getFactory(); + Http2 http2 = new Http2(); + http2.setEnabled(true); + factory.setHttp2(http2); + this.webServer = factory.getWebServer(new EchoHandler()); + this.webServer.start(); + try (CloseableHttpAsyncClient http2Client = HttpAsyncClients.createHttp2Default()) { + http2Client.start(); + SimpleHttpRequest request = SimpleHttpRequests.post("http://localhost:" + this.webServer.getPort()); + request.setBody("Hello World", ContentType.TEXT_PLAIN); + SimpleHttpResponse response = http2Client.execute(request, new FutureCallback() { + + @Override + public void failed(Exception ex) { + } + + @Override + public void completed(SimpleHttpResponse result) { + } + + @Override + public void cancelled() { + } + + }).get(); + assertThat(response.getCode() == HttpStatus.OK.value()); + assertThat(response.getBodyText()).isEqualTo("Hello World"); + } + } + protected WebClient prepareCompressionTest() { Compression compression = new Compression(); compression.setEnabled(true); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index eb5d64c3a9b5..625e833d6986 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -49,6 +49,7 @@ import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.Callable; import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.RunnableFuture; @@ -78,6 +79,12 @@ import javax.servlet.http.HttpSession; import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequests; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.InputStreamFactory; @@ -112,6 +119,7 @@ import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.GracefulShutdownResult; +import org.springframework.boot.web.server.Http2; import org.springframework.boot.web.server.MimeMappings; import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.Shutdown; @@ -1121,6 +1129,39 @@ void whenARequestIsActiveThenStopWillComplete() throws InterruptedException, Bro } } + @Test + void whenHttp2IsEnabledAndSslIsDisabledThenH2cCanBeUsed() + throws InterruptedException, ExecutionException, IOException { + AbstractServletWebServerFactory factory = getFactory(); + Http2 http2 = new Http2(); + http2.setEnabled(true); + factory.setHttp2(http2); + this.webServer = factory.getWebServer(exampleServletRegistration()); + this.webServer.start(); + try (CloseableHttpAsyncClient http2Client = HttpAsyncClients.createHttp2Default()) { + http2Client.start(); + SimpleHttpRequest request = SimpleHttpRequests + .get("http://localhost:" + this.webServer.getPort() + "/hello"); + SimpleHttpResponse response = http2Client.execute(request, new FutureCallback() { + + @Override + public void failed(Exception ex) { + } + + @Override + public void completed(SimpleHttpResponse result) { + } + + @Override + public void cancelled() { + } + + }).get(); + assertThat(response.getCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.getBodyText()).isEqualTo("Hello World"); + } + } + protected Future initiateGetRequest(int port, String path) { return initiateGetRequest(HttpClients.createMinimal(), port, path); }