Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP/3 support #8559

Merged
merged 19 commits into from
Mar 9, 2023
Merged
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ managed-jackson-databind = "2.14.1"
managed-maven-native-plugin = "0.9.13"
managed-methvin-directory-watcher = "0.16.1"
managed-netty = "4.1.87.Final"
managed-netty-http3 = "0.0.16.Final"
managed-reactive-streams = "1.0.4"
# This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM
managed-reactor = "3.4.24"
Expand Down Expand Up @@ -115,6 +116,7 @@ managed-methvin-directoryWatcher = { module = "io.methvin:directory-watcher", ve
managed-netty-buffer = { module = "io.netty:netty-buffer", version.ref = "managed-netty" }
managed-netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "managed-netty" }
managed-netty-codec-http2 = { module = "io.netty:netty-codec-http2", version.ref = "managed-netty" }
managed-netty-incubator-codec-http3 = { module = "io.netty.incubator:netty-incubator-codec-http3", version.ref = "managed-netty-http3" }
managed-netty-handler = { module = "io.netty:netty-handler", version.ref = "managed-netty" }
managed-netty-handler-proxy = { module = "io.netty:netty-handler-proxy", version.ref = "managed-netty" }
managed-netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "managed-netty" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public final class HttpVersionSelection {
* ALPN protocol ID for HTTP/2.
*/
public static final String ALPN_HTTP_2 = "h2";
/**
* ALPN protocol ID for HTTP/3. When this is selected, it must be the only ALPN ID, since we
* will connect via UDP.
*/
public static final String ALPN_HTTP_3 = "h3";

private static final HttpVersionSelection LEGACY_1 = new HttpVersionSelection(
PlaintextMode.HTTP_1,
Expand All @@ -59,12 +64,17 @@ public final class HttpVersionSelection {
private final boolean alpn;
private final String[] alpnSupportedProtocols;
private final boolean http2CipherSuites;
private final boolean http3;

private HttpVersionSelection(@NonNull PlaintextMode plaintextMode, boolean alpn, @NonNull String[] alpnSupportedProtocols, boolean http2CipherSuites) {
this.plaintextMode = plaintextMode;
this.alpn = alpn;
this.alpnSupportedProtocols = alpnSupportedProtocols;
this.http2CipherSuites = http2CipherSuites;
this.http3 = Arrays.asList(alpnSupportedProtocols).contains(ALPN_HTTP_3);
if (http3 && alpnSupportedProtocols.length != 1) {
throw new IllegalArgumentException("When using HTTP 3, h3 must be the only ALPN protocol");
}
}

/**
Expand Down Expand Up @@ -181,6 +191,11 @@ public boolean isHttp2CipherSuites() {
return http2CipherSuites;
}

@Internal
public boolean isHttp3() {
return http3;
}

/**
* The connection mode to use for plaintext (non-TLS) connections.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static io.micronaut.http.client.exceptions.HttpClientExceptionUtils.populateServiceId;
Expand All @@ -69,6 +71,8 @@
abstract class AbstractJdkHttpClient {

public static final String H2C_ERROR_MESSAGE = "H2C is not supported by the JDK HTTP client";
public static final String H3_ERROR_MESSAGE = "HTTP/3 is not supported by the JDK HTTP client";
public static final String WEIRD_ALPN_ERROR_MESSAGE = "The only supported ALPN modes are [" + HttpVersionSelection.ALPN_HTTP_1 + "] or [" + HttpVersionSelection.ALPN_HTTP_1 + "," + HttpVersionSelection.ALPN_HTTP_2 + "]";

protected final LoadBalancer loadBalancer;
protected final HttpVersionSelection httpVersion;
Expand Down Expand Up @@ -142,9 +146,22 @@ protected AbstractJdkHttpClient(
if (httpVersionSelection.getPlaintextMode() == HttpVersionSelection.PlaintextMode.H2C) {
throw new ConfigurationException(H2C_ERROR_MESSAGE);
}
if (httpVersionSelection.isHttp3()) {
throw new ConfigurationException(H3_ERROR_MESSAGE);
}

if (httpVersionSelection.isAlpn() && httpVersionSelection.isHttp2CipherSuites()) {
builder.version(HttpClient.Version.HTTP_2);
if (httpVersionSelection.isAlpn()) {
List<String> supportedProtocols = Arrays.asList(httpVersionSelection.getAlpnSupportedProtocols());
if (supportedProtocols.size() == 2 &&
supportedProtocols.contains(HttpVersionSelection.ALPN_HTTP_1) &&
supportedProtocols.contains(HttpVersionSelection.ALPN_HTTP_2)) {
builder.version(HttpClient.Version.HTTP_2);
} else if (supportedProtocols.size() == 1 &&
supportedProtocols.get(0).equals(HttpVersionSelection.ALPN_HTTP_1)) {
builder.version(HttpClient.Version.HTTP_1_1);
} else {
throw new ConfigurationException(WEIRD_ALPN_ERROR_MESSAGE);
}
} else {
builder.version(HttpClient.Version.HTTP_1_1);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,40 @@ class H2CSpec extends Specification {
cleanup:
ctx.close()
}

def "h3 is not supported"() {
given:
def ctx = ApplicationContext.run(
'micronaut.http.client.alpn-modes': ['h3']
)

when:
ctx.getBean(HttpClient)

then:
def ex = thrown(BeanInstantiationException)
ex.cause instanceof ConfigurationException
ex.cause.message == AbstractJdkHttpClient.H3_ERROR_MESSAGE

cleanup:
ctx.close()
}

def "http2-only is not supported"() {
given:
def ctx = ApplicationContext.run(
'micronaut.http.client.alpn-modes': ['h2']
)

when:
ctx.getBean(HttpClient)

then:
def ex = thrown(BeanInstantiationException)
ex.cause instanceof ConfigurationException
ex.cause.message == AbstractJdkHttpClient.WEIRD_ALPN_ERROR_MESSAGE

cleanup:
ctx.close()
}
}
2 changes: 2 additions & 0 deletions http-client/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ dependencies {
api project(":http-netty")
api libs.managed.netty.handler.proxy

compileOnly libs.managed.netty.incubator.codec.http3

testAnnotationProcessor platform(libs.test.boms.micronaut.validation)
testAnnotationProcessor (libs.micronaut.validation.processor) {
exclude group: 'io.micronaut'
Expand Down
Loading