This is an automated email from the ASF dual-hosted git repository.
kwin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
The following commit(s) were added to refs/heads/master by this push:
new a0df5d58a Support HTTP/3 in Jetty and JRE HTTP Client (#1949)
a0df5d58a is described below
commit a0df5d58a1aaaf6ca8f87fa2638fd1fae4b0df7d
Author: Konrad Windszus <[email protected]>
AuthorDate: Thu Jul 16 17:30:22 2026 +0200
Support HTTP/3 in Jetty and JRE HTTP Client (#1949)
Consolidate http version configuration among HTTP transporters
Default to HTTP/2 in all transporters except Apache HTTP Client 4.x
Build additionally with Java 26 in GHA
IT: Clean up and regenerate key stores
This closes #1760
---
.github/workflows/maven-verify.yml | 2 +-
.../eclipse/aether/ConfigurationProperties.java | 34 ++++
.../maven-resolver-demo-snippets/pom.xml | 25 ++-
maven-resolver-test-http/pom.xml | 13 ++
.../aether/internal/test/util/http/HttpServer.java | 113 +++++++++---
.../test/util/http/HttpTransporterTest.java | 201 ++++++++++++++++-----
.../src/main/resources/ssl/README.txt | 8 +-
.../src/main/resources/ssl/client-store | Bin 2244 -> 3612 bytes
.../src/main/resources/ssl/server-store | Bin 2246 -> 3612 bytes
.../src/main/resources/ssl/server-store-selfsigned | Bin 2750 -> 0 bytes
maven-resolver-tools/pom.xml | 19 ++
.../src/main/resources/configuration.md.vm | 1 +
.../transport/apache/ApacheRFC9457Reporter.java | 3 +
.../transport/apache/ApacheTransporterTest.java | 19 +-
.../maven-resolver-transport-jdk11/pom.xml | 6 +
.../aether/transport/jdk/JdkTransporter.java | 69 ++++++-
.../aether/transport/jdk/JdkTransporterCloser.java | 3 +-
.../jdk/JdkTransporterConfigurationKeys.java | 2 +
.../aether/transport/jdk/JdkTransporterTest.java | 58 +++++-
maven-resolver-transport-jetty/pom.xml | 31 ++++
.../aether/transport/jetty/JettyTransporter.java | 92 ++++++----
.../transport/jetty/JettyTransporterTest.java | 28 +++
.../java/org/eclipse/aether/util/ConfigUtils.java | 44 +++++
.../transport/http/HttpTransporterUtils.java | 13 ++
.../org/eclipse/aether/util/ConfigUtilsTest.java | 25 +++
src/site/markdown/transporter-known-issues.md | 23 ++-
26 files changed, 689 insertions(+), 143 deletions(-)
diff --git a/.github/workflows/maven-verify.yml
b/.github/workflows/maven-verify.yml
index 7df1c36ee..fa02b6dcc 100644
--- a/.github/workflows/maven-verify.yml
+++ b/.github/workflows/maven-verify.yml
@@ -30,4 +30,4 @@ jobs:
ff-site-run: false
maven4-enabled: true
verify-fail-fast: false
- jdk-matrix: '[ "21", "25" ]'
+ jdk-matrix: '[ "21", "25", "26" ]'
diff --git
a/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
b/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
index d9d9ce391..61025ace4 100644
---
a/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
+++
b/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
@@ -514,6 +514,40 @@ public final class ConfigurationProperties {
*/
public static final String HTTPS_SECURITY_MODE_INSECURE = "insecure";
+ public enum HttpVersion {
+ /**
+ * The default HTTP version supported by the respective transporter
(the most recent stable one, usually HTTP/2)
+ */
+ DEFAULT,
+ HTTP_1_1,
+ HTTP_2,
+ HTTP_3,
+ /**
+ * The maximum HTTP version supported by the respective transporter
(may be unstable).
+ */
+ MAXIMUM;
+ }
+
+ /**
+ * The maximum and preferred HTTP version. Some transporters transparently
fall back to lower versions if remote server does not
+ * support the requested version, while other may just simply fail the
request.
+ * Value must be a {@link HttpVersion} enum value or its String
representation. Default is {@link #DEFAULT_HTTP_VERSION}.
+ *
+ * @configurationSource {@link
RepositorySystemSession#getConfigProperties()}
+ * @configurationType {@link ConfigurationProperties.HttpVersion}
+ * @configurationDefaultValue {@link #DEFAULT_HTTP_VERSION}
+ * @configurationRepoIdSuffix Yes
+ * @since NEXT
+ */
+ public static final String HTTP_VERSION = PREFIX_TRANSPORT_HTTP +
"version";
+
+ /**
+ * Default value if {@link #HTTP_VERSION} is not set.
+ *
+ * @since NEXT
+ */
+ public static final HttpVersion DEFAULT_HTTP_VERSION = HttpVersion.DEFAULT;
+
/**
* A flag indicating which visitor should be used to "flatten" the
dependency graph into list. In Maven 4
* the default is new "levelOrder", while Maven 3 used "preOrder". This
property accepts values
diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml
b/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml
index 2da27f51a..d77ae4eba 100644
--- a/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml
+++ b/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml
@@ -53,7 +53,6 @@
</dependency>
</dependencies>
</dependencyManagement>
-
<dependencies>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
@@ -147,4 +146,28 @@
<scope>test</scope>
</dependency>
</dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-enforcer-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>enforce-bytecode-version</id>
+ <configuration>
+ <rules>
+ <enforceBytecodeVersion>
+ <excludes>
+ <!-- this requires Java 22 but is optional only -->
+
<exclude>org.eclipse.jetty.quic:jetty-quic-quiche-foreign</exclude>
+ </excludes>
+ </enforceBytecodeVersion>
+ </rules>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
</project>
diff --git a/maven-resolver-test-http/pom.xml b/maven-resolver-test-http/pom.xml
index 6845de444..9521f6c74 100644
--- a/maven-resolver-test-http/pom.xml
+++ b/maven-resolver-test-http/pom.xml
@@ -77,6 +77,19 @@
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-http2-server</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http3</groupId>
+ <artifactId>jetty-http3-server</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.quic</groupId>
+ <artifactId>jetty-quic-quiche-server</artifactId>
+ </dependency>
+ <!-- no transitive dependency of jetty-quic-quiche-server,
https://github.com/jetty/jetty.project/issues/15385 -->
+ <dependency>
+ <groupId>org.eclipse.jetty.quic</groupId>
+ <artifactId>jetty-quic-quiche-jna</artifactId>
+ </dependency>
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-server</artifactId>
diff --git
a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java
b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java
index ecffdd118..f7aacb35f 100644
---
a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java
+++
b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java
@@ -51,12 +51,17 @@ import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
+import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory;
+import org.eclipse.jetty.http3.server.HTTP3ServerQuicConfiguration;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.quic.quiche.server.QuicheServerConnector;
+import org.eclipse.jetty.quic.quiche.server.QuicheServerQuicConfiguration;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
@@ -75,6 +80,7 @@ import org.slf4j.LoggerFactory;
public class HttpServer {
public static class LogEntry {
+ private final HttpVersion version;
private final String method;
@@ -86,12 +92,17 @@ public class HttpServer {
CountDownLatch responseHeadersAvailableSignal = new CountDownLatch(1);
- public LogEntry(String method, String path, Map<String, String>
requestHeaders) {
+ public LogEntry(HttpVersion version, String method, String path,
Map<String, String> requestHeaders) {
+ this.version = version;
this.method = method;
this.path = path;
this.requestHeaders = requestHeaders;
}
+ public HttpVersion getVersion() {
+ return version;
+ }
+
public String getMethod() {
return method;
}
@@ -127,7 +138,7 @@ public class HttpServer {
@Override
public String toString() {
- return method + " " + path;
+ return version + " " + method + " " + path;
}
}
@@ -160,6 +171,8 @@ public class HttpServer {
private ServerConnector httpsConnector;
+ private QuicheServerConnector http3Connector;
+
private String username;
private String password;
@@ -188,6 +201,10 @@ public class HttpServer {
return httpsConnector != null ? httpsConnector.getLocalPort() : -1;
}
+ public int getHttp3Port() {
+ return http3Connector != null ? http3Connector.getLocalPort() : -1;
+ }
+
public String getHttpUrl() {
return "http://" + getHost() + ":" + getHttpPort();
}
@@ -196,37 +213,29 @@ public class HttpServer {
return "https://" + getHost() + ":" + getHttpsPort();
}
- public HttpServer addSslConnector() {
- return addSslConnector(true, true);
+ public String getHttp3Url() {
+ return "https://" + getHost() + ":" + getHttp3Port();
+ }
+
+ public HttpServer addHttp2ConnectorWithMutualTLS() {
+ return addHttp2Connector(true, true);
}
- public HttpServer addSelfSignedSslConnector() {
- return addSslConnector(false, true);
+ public HttpServer addHttp2Connector() {
+ return addHttp2Connector(false, true);
}
- public HttpServer addSelfSignedSslConnectorHttp2Only() {
- return addSslConnector(false, false);
+ public HttpServer addHttp2OnlyConnector() {
+ return addHttp2Connector(false, false);
}
- private HttpServer addSslConnector(boolean needClientAuth, boolean
needHttp11) {
+ public HttpServer addHttp2OnlyConnectorWithMutualTLS() {
+ return addHttp2Connector(true, false);
+ }
+
+ private HttpServer addHttp2Connector(boolean needClientAuth, boolean
needHttp11) {
if (httpsConnector == null) {
- SslContextFactory.Server ssl = new SslContextFactory.Server();
- ssl.setNeedClientAuth(needClientAuth);
- if (!needClientAuth) {
-
ssl.setKeyStorePath(HttpTransporterTest.KEY_STORE_SELF_SIGNED_PATH
- .toAbsolutePath()
- .toString());
- ssl.setKeyStorePassword("server-pwd");
- ssl.setSniRequired(false);
- } else {
- ssl.setKeyStorePath(
-
HttpTransporterTest.KEY_STORE_PATH.toAbsolutePath().toString());
- ssl.setKeyStorePassword("server-pwd");
- ssl.setTrustStorePath(
-
HttpTransporterTest.TRUST_STORE_PATH.toAbsolutePath().toString());
- ssl.setTrustStorePassword("client-pwd");
- ssl.setSniRequired(false);
- }
+ SslContextFactory.Server ssl =
createServerSslContextFactory(needClientAuth);
HttpConfiguration httpsConfig = new HttpConfiguration();
SecureRequestCustomizer customizer = new SecureRequestCustomizer();
@@ -259,6 +268,47 @@ public class HttpServer {
return this;
}
+ private SslContextFactory.Server createServerSslContextFactory(boolean
needClientAuth) {
+ SslContextFactory.Server ssl = new SslContextFactory.Server();
+ ssl.setSniRequired(false);
+ ssl.setKeyStorePath(
+
HttpTransporterTest.SERVER_STORE_PATH.toAbsolutePath().toString());
+ ssl.setKeyStorePassword("server-pwd");
+ ssl.setNeedClientAuth(needClientAuth);
+ if (needClientAuth) {
+ ssl.setTrustStorePath(
+
HttpTransporterTest.CLIENT_STORE_PATH.toAbsolutePath().toString());
+ ssl.setTrustStorePassword("client-pwd");
+ }
+ return ssl;
+ }
+
+ public HttpServer addHttp3Connector(boolean needClientAuth) {
+ return addHttp3Connector(needClientAuth, -1);
+ }
+
+ public HttpServer addHttp3Connector(boolean needClientAuth, int port) {
+ if (http3Connector == null) {
+ QuicheServerQuicConfiguration serverQuicConfig =
HTTP3ServerQuicConfiguration.configure(
+ new
QuicheServerQuicConfiguration(HttpTransporterTest.PEM_QUICHE_SERVER_PATH));
+ http3Connector = new QuicheServerConnector(
+ server,
+ createServerSslContextFactory(needClientAuth),
+ serverQuicConfig,
+ new HTTP3ServerConnectionFactory());
+ if (port != -1) {
+ http3Connector.setPort(port);
+ }
+ server.addConnector(http3Connector);
+ try {
+ http3Connector.start();
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ return this;
+ }
+
public List<LogEntry> getLogEntries() {
return logEntries;
}
@@ -322,6 +372,7 @@ public class HttpServer {
server = new Server();
httpConnector = new ServerConnector(server);
+ // always add the HTTP 1.1 connector
server.addConnector(httpConnector);
server.setHandler(new LogHandler(new CompressionEnforcingHandler(new
Handler.Sequence(
@@ -484,14 +535,19 @@ public class HttpServer {
public boolean handle(Request req, Response response, Callback
callback) throws Exception {
LOGGER.info(
- "{} {}{}",
+ "{} {} {}{}",
+ req.getConnectionMetaData().getHttpVersion(),
req.getMethod(),
req.getHttpURI().getDecodedPath(),
req.getHttpURI().getQuery() != null ? "?" +
req.getHttpURI().getQuery() : "");
Map<String, String> requestHeaders =
toUnmodifiableMap(req.getHeaders()); // capture request
headers before other handlers modify them
- LogEntry logEntry = new LogEntry(req.getMethod(),
req.getHttpURI().getPathQuery(), requestHeaders);
+ LogEntry logEntry = new LogEntry(
+ req.getConnectionMetaData().getHttpVersion(),
+ req.getMethod(),
+ req.getHttpURI().getPathQuery(),
+ requestHeaders);
logEntries.add(logEntry);
// prevent closing the response before logging (assume all writes
are synchronous for simplicity)
boolean result = super.handle(req, response, callback);
@@ -602,6 +658,7 @@ public class HttpServer {
Content.copy(req, Content.Sink.from(channel),
fileWriteCallback);
fileWriteCallback.block();
} catch (IOException e) {
+ LOGGER.warn("Failed to write file {}",
file.getAbsolutePath(), e);
file.delete();
throw e;
}
diff --git
a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
index 00bbbc242..c72964c4d 100644
---
a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
+++
b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
@@ -29,7 +29,6 @@ import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.URI;
-import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -69,12 +68,15 @@ import
org.eclipse.aether.spi.connector.transport.http.RFC9457.HttpRFC9457Except
import org.eclipse.aether.transfer.NoTransporterException;
import org.eclipse.aether.transfer.TransferCancelledException;
import org.eclipse.aether.util.repository.AuthenticationBuilder;
+import org.eclipse.jetty.http.HttpVersion;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -86,19 +88,21 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/**
* Common set of tests against Http transporter.
*/
@SuppressWarnings({"checkstyle:MethodName"})
+@TestMethodOrder(MethodOrderer.MethodName.class)
public abstract class HttpTransporterTest {
- protected static final Path KEY_STORE_PATH = Paths.get("target/keystore");
+ protected static final Path SERVER_STORE_PATH =
Paths.get("target/server-store");
- protected static final Path KEY_STORE_SELF_SIGNED_PATH =
Paths.get("target/keystore-self-signed");
+ protected static final Path CLIENT_STORE_PATH =
Paths.get("target/client-store");
- protected static final Path TRUST_STORE_PATH =
Paths.get("target/trustStore");
+ protected static final Path PEM_QUICHE_SERVER_PATH =
Paths.get("target/pems");
protected static SSLContext defaultSslContext;
@@ -110,18 +114,14 @@ public abstract class HttpTransporterTest {
@BeforeAll
protected static void beforeAll() throws NoSuchAlgorithmException {
// populate custom keystore and truststore
- URL keyStoreUrl =
HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store");
- URL keyStoreSelfSignedUrl =
-
HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store-selfsigned");
- URL trustStoreUrl =
HttpTransporterTest.class.getClassLoader().getResource("ssl/client-store");
-
try {
- try (InputStream keyStoreStream = keyStoreUrl.openStream();
- InputStream keyStoreSelfSignedStream =
keyStoreSelfSignedUrl.openStream();
- InputStream trustStoreStream = trustStoreUrl.openStream())
{
- Files.copy(keyStoreStream, KEY_STORE_PATH,
StandardCopyOption.REPLACE_EXISTING);
- Files.copy(keyStoreSelfSignedStream,
KEY_STORE_SELF_SIGNED_PATH, StandardCopyOption.REPLACE_EXISTING);
- Files.copy(trustStoreStream, TRUST_STORE_PATH,
StandardCopyOption.REPLACE_EXISTING);
+ try (InputStream keyStoreStream =
+
HttpTransporterTest.class.getClassLoader().getResourceAsStream("ssl/server-store");
+ InputStream trustStoreStream =
+
HttpTransporterTest.class.getClassLoader().getResourceAsStream("ssl/client-store");
) {
+ Files.copy(keyStoreStream, SERVER_STORE_PATH,
StandardCopyOption.REPLACE_EXISTING);
+ Files.copy(trustStoreStream, CLIENT_STORE_PATH,
StandardCopyOption.REPLACE_EXISTING);
+ Files.createDirectories(PEM_QUICHE_SERVER_PATH);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
@@ -129,7 +129,7 @@ public abstract class HttpTransporterTest {
// override default SSLContext to include our custom keystore and
truststore (which are "cross connected" with
// HttpServer)
defaultSslContext = SSLContext.getDefault();
- SSLContext.setDefault(createSSLContext());
+ SSLContext.setDefault(createClientSSLContext());
}
@AfterAll
@@ -140,23 +140,23 @@ public abstract class HttpTransporterTest {
}
/**
- * Creates an {@link SSLContext} that extends the default keystore and
truststore with the entries
- * from {@link #KEY_STORE_PATH} (password {@code "server-pwd"}) and {@link
#TRUST_STORE_PATH}
+ * Creates an {@link SSLContext} for the client that extends the default
keystore and truststore with the entries
+ * from {@link #SERVER_STORE_PATH} (password {@code "server-pwd"}) and
{@link #CLIENT_STORE_PATH}
* (password {@code "client-pwd"}).
*
* @return an {@link SSLContext} combining default and custom key/trust
material
*/
- protected static SSLContext createSSLContext() {
+ protected static SSLContext createClientSSLContext() {
try {
// Load custom key store (KEY_STORE_PATH acts as truststore in
"cross connected" setup)
- KeyStore customTrustStore = KeyStore.getInstance("jks");
- try (InputStream is = Files.newInputStream(KEY_STORE_PATH)) {
+ KeyStore customTrustStore = KeyStore.getInstance("pkcs12");
+ try (InputStream is = Files.newInputStream(SERVER_STORE_PATH)) {
customTrustStore.load(is, "server-pwd".toCharArray());
}
// Load custom trust store (TRUST_STORE_PATH acts as keystore in
"cross connected" setup)
- KeyStore customKeyStore = KeyStore.getInstance("jks");
- try (InputStream is = Files.newInputStream(TRUST_STORE_PATH)) {
+ KeyStore customKeyStore = KeyStore.getInstance("pkcs12");
+ try (InputStream is = Files.newInputStream(CLIENT_STORE_PATH)) {
customKeyStore.load(is, "client-pwd".toCharArray());
}
@@ -285,6 +285,7 @@ public abstract class HttpTransporterTest {
TestFileUtils.writeString(resumable, "resumable");
resumable.setLastModified(System.currentTimeMillis() - 90 * 1000);
httpServer = new HttpServer().setRepoDir(repoDir).start();
+ // always create a transporter connecting to the Http URL
newTransporter(httpServer.getHttpUrl());
}
@@ -318,6 +319,29 @@ public abstract class HttpTransporterTest {
return true;
}
+ /**
+ * Indicates whether the transporter implementation supports HTTP/3.
+ * @return {@code true} if HTTP/3 is supported, {@code false} otherwise.
+ */
+ protected boolean supportsHttp3() {
+ // skip on ASF Jenkins due to incompatible GLIBC version
+ // (https://github.com/jetty-project/jetty-quiche-native/issues/180 and
+ // https://issues.apache.org/jira/browse/INFRA-28128)
+ // identified via property "os.version" exposed in
https://ci-maven.apache.org/computer/maven6/systemInfo
+ assumeFalse(
+ System.getProperty("os.version").equals("5.15.0-1089-azure"),
+ "Skipping HTTP/3 tests on ASF Jenkins Linux Nodes");
+ return true;
+ }
+
+ /**
+ * Indicates whether the transporter implementation supports HTTP/2.
+ * @return {@code true} if HTTP/2 is supported, {@code false} otherwise.
+ */
+ protected boolean supportsHttp2() {
+ return true;
+ }
+
@Test
protected void testClassify() {
assertEquals(Transporter.ERROR_OTHER, transporter.classify(new
FileNotFoundException()));
@@ -483,14 +507,14 @@ public abstract class HttpTransporterTest {
@Test
protected void testPeek_SSL() throws Exception {
- httpServer.addSslConnector();
+ httpServer.addHttp2ConnectorWithMutualTLS();
newTransporter(httpServer.getHttpsUrl());
transporter.peek(new PeekTask(URI.create("repo/file.txt")));
}
@Test
protected void testPeek_Redirect() throws Exception {
- httpServer.addSslConnector();
+ httpServer.addHttp2ConnectorWithMutualTLS();
transporter.peek(new PeekTask(URI.create("redirect/file.txt")));
transporter.peek(new
PeekTask(URI.create("redirect/file.txt?scheme=https")));
}
@@ -737,7 +761,7 @@ public abstract class HttpTransporterTest {
@Test
protected void testGet_SSL() throws Exception {
- httpServer.addSslConnector();
+ httpServer.addHttp2ConnectorWithMutualTLS();
newTransporter(httpServer.getHttpsUrl());
RecordingTransportListener listener = new RecordingTransportListener();
GetTask task = new
GetTask(URI.create("repo/file.txt")).setListener(listener);
@@ -753,7 +777,7 @@ public abstract class HttpTransporterTest {
@Test
protected void testGet_SSL_WithServerErrors() throws Exception {
httpServer.setServerErrorsBeforeWorks(1);
- httpServer.addSslConnector();
+ httpServer.addHttp2ConnectorWithMutualTLS();
newTransporter(httpServer.getHttpsUrl());
for (int i = 1; i < 3; i++) {
try {
@@ -775,7 +799,7 @@ public abstract class HttpTransporterTest {
@Test
protected void testGet_HTTPS_Unknown_SecurityMode() throws Exception {
session.setConfigProperty(ConfigurationProperties.HTTPS_SECURITY_MODE,
"unknown");
- httpServer.addSelfSignedSslConnector();
+ httpServer.addHttp2Connector();
try {
newTransporter(httpServer.getHttpsUrl());
fail("Unsupported security mode");
@@ -786,12 +810,66 @@ public abstract class HttpTransporterTest {
@Test
protected void testGet_HTTPS_Insecure_SecurityMode() throws Exception {
- // here we use alternate server-store-selfigned key (as the key set it
static initializer is probably already
- // used to init SSLContext/SSLSocketFactory/etc
- session.setConfigProperty(
- ConfigurationProperties.HTTPS_SECURITY_MODE,
ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
- httpServer.addSelfSignedSslConnector();
- newTransporter(httpServer.getHttpsUrl());
+ // we have to reset the default ssl context to avoid the default
truststore being used, which would make the
+ // test pass even if the security mode is not set to insecure
+ SSLContext.setDefault(defaultSslContext);
+ try {
+ session.setConfigProperty(
+ ConfigurationProperties.HTTPS_SECURITY_MODE,
ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
+ httpServer.addHttp2Connector();
+ newTransporter(httpServer.getHttpsUrl());
+ RecordingTransportListener listener = new
RecordingTransportListener();
+ GetTask task = new
GetTask(URI.create("repo/file.txt")).setListener(listener);
+ transporter.get(task);
+ assertEquals("test", task.getDataString());
+ assertEquals(0L, listener.getDataOffset());
+ assertEquals(4L, listener.getDataLength());
+ assertEquals(1, listener.getStartedCount());
+ assertTrue(listener.getProgressedCount() > 0, "Count: " +
listener.getProgressedCount());
+ assertEquals(task.getDataString(),
listener.getBaos().toString(StandardCharsets.UTF_8));
+ } finally {
+ // restore the default SSL context used for all other tests
+ SSLContext.setDefault(createClientSSLContext());
+ }
+ }
+
+ @Test
+ protected void testGet_HTTPS_HTTP2Only_Insecure_SecurityMode() throws
Exception {
+ assumeTrue(supportsHttp2(), "Transporter does not support HTTP/2");
+ // we have to reset the default ssl context to avoid the default
truststore being used, which would make the
+ // test pass even if the security mode is not set to insecure
+ SSLContext.setDefault(defaultSslContext);
+ try {
+ session.setConfigProperty(ConfigurationProperties.HTTP_VERSION,
ConfigurationProperties.HttpVersion.HTTP_2);
+ session.setConfigProperty(
+ ConfigurationProperties.HTTPS_SECURITY_MODE,
ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
+ httpServer.addHttp2OnlyConnector();
+ newTransporter(httpServer.getHttpsUrl());
+ RecordingTransportListener listener = new
RecordingTransportListener();
+ GetTask task = new
GetTask(URI.create("repo/file.txt")).setListener(listener);
+ transporter.get(task);
+ assertEquals("test", task.getDataString());
+ assertEquals(0L, listener.getDataOffset());
+ assertEquals(4L, listener.getDataLength());
+ assertEquals(1, listener.getStartedCount());
+ assertTrue(listener.getProgressedCount() > 0, "Count: " +
listener.getProgressedCount());
+ assertEquals(task.getDataString(),
listener.getBaos().toString(StandardCharsets.UTF_8));
+ httpServer.getLogEntries().forEach(log -> {
+ assertEquals(HttpVersion.HTTP_2, log.getVersion());
+ });
+ } finally {
+ // restore the default SSL context used for all other tests
+ SSLContext.setDefault(createClientSSLContext());
+ }
+ }
+
+ @Test
+ protected void testGet_HTTP3Only() throws Exception {
+ assumeTrue(supportsHttp3(), "Transporter does not support HTTP/3");
+ session.setConfigProperty(ConfigurationProperties.HTTP_VERSION,
ConfigurationProperties.HttpVersion.HTTP_3);
+ httpServer.addHttp3Connector(false);
+ httpServer.start();
+ newTransporter(httpServer.getHttp3Url());
RecordingTransportListener listener = new RecordingTransportListener();
GetTask task = new
GetTask(URI.create("repo/file.txt")).setListener(listener);
transporter.get(task);
@@ -801,16 +879,48 @@ public abstract class HttpTransporterTest {
assertEquals(1, listener.getStartedCount());
assertTrue(listener.getProgressedCount() > 0, "Count: " +
listener.getProgressedCount());
assertEquals(task.getDataString(),
listener.getBaos().toString(StandardCharsets.UTF_8));
+ httpServer.getLogEntries().forEach(log -> {
+ assertEquals(HttpVersion.HTTP_3, log.getVersion());
+ });
}
@Test
- protected void testGet_HTTPS_HTTP2Only_Insecure_SecurityMode() throws
Exception {
- // here we use alternate server-store-selfigned key (as the key set it
static initializer is probably already
- // used to init SSLContext/SSLSocketFactory/etc
- enableHttp2Protocol();
- session.setConfigProperty(
- ConfigurationProperties.HTTPS_SECURITY_MODE,
ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
- httpServer.addSelfSignedSslConnectorHttp2Only();
+ protected void testGet_HTTP3() throws Exception {
+ assumeTrue(supportsHttp3(), "Transporter does not support HTTP/3");
+ session.setConfigProperty(ConfigurationProperties.HTTP_VERSION,
ConfigurationProperties.HttpVersion.HTTP_3);
+ // both HTTP2 and HTTP3 endpoints are available at the same port
+ httpServer.addHttp2OnlyConnectorWithMutualTLS();
+ httpServer.addHttp3Connector(false, httpServer.getHttpsPort());
+ // alt-svc header should point to http/3
+ httpServer.start();
+ newTransporter(httpServer.getHttp3Url());
+ RecordingTransportListener listener = new RecordingTransportListener();
+ GetTask task = new
GetTask(URI.create("repo/file.txt")).setListener(listener);
+ // issue 2 requests to ensure that the second request is HTTP/3 (the
first one may be HTTP/2 if the TCP
+ // connection is faster)
+ transporter.get(task);
+ transporter.get(task);
+ assertEquals("test", task.getDataString());
+ assertEquals(0L, listener.getDataOffset());
+ assertEquals(4L, listener.getDataLength());
+ assertEquals(2, listener.getStartedCount());
+ assertTrue(listener.getProgressedCount() > 0, "Count: " +
listener.getProgressedCount());
+ assertEquals(task.getDataString(),
listener.getBaos().toString(StandardCharsets.UTF_8));
+ // the last request should be HTTP/3 finally
+ assertEquals(
+ HttpVersion.HTTP_3,
+ httpServer
+ .getLogEntries()
+ .get(httpServer.getLogEntries().size() - 1)
+ .getVersion());
+ }
+
+ @Test
+ protected void testGet_HTTP3FallbackToHTTP2() throws Exception {
+ assumeTrue(supportsHttp3(), "Transporter does not support HTTP/3");
+ session.setConfigProperty(ConfigurationProperties.HTTP_VERSION,
ConfigurationProperties.HttpVersion.HTTP_3);
+ httpServer.addHttp2OnlyConnectorWithMutualTLS();
+ httpServer.start();
newTransporter(httpServer.getHttpsUrl());
RecordingTransportListener listener = new RecordingTransportListener();
GetTask task = new
GetTask(URI.create("repo/file.txt")).setListener(listener);
@@ -821,13 +931,14 @@ public abstract class HttpTransporterTest {
assertEquals(1, listener.getStartedCount());
assertTrue(listener.getProgressedCount() > 0, "Count: " +
listener.getProgressedCount());
assertEquals(task.getDataString(),
listener.getBaos().toString(StandardCharsets.UTF_8));
+ httpServer.getLogEntries().forEach(log -> {
+ assertEquals(HttpVersion.HTTP_2, log.getVersion());
+ });
}
- protected void enableHttp2Protocol() {}
-
@Test
protected void testGet_Redirect() throws Exception {
- httpServer.addSslConnector();
+ httpServer.addHttp2ConnectorWithMutualTLS();
RecordingTransportListener listener = new RecordingTransportListener();
GetTask task = new
GetTask(URI.create("redirect/file.txt?scheme=https")).setListener(listener);
transporter.get(task);
@@ -1215,7 +1326,7 @@ public abstract class HttpTransporterTest {
@Test
protected void testPut_SSL() throws Exception {
- httpServer.addSslConnector();
+ httpServer.addHttp2ConnectorWithMutualTLS();
httpServer.setAuthentication("testuser", "testpass");
auth = new AuthenticationBuilder()
.addUsername("testuser")
diff --git a/maven-resolver-test-http/src/main/resources/ssl/README.txt
b/maven-resolver-test-http/src/main/resources/ssl/README.txt
index b1be71c2c..d136fa4ac 100644
--- a/maven-resolver-test-http/src/main/resources/ssl/README.txt
+++ b/maven-resolver-test-http/src/main/resources/ssl/README.txt
@@ -1,5 +1,9 @@
client-store generated via
-> keytool -genkey -alias localhost -keypass client-pwd -keystore client-store
-storepass client-pwd -validity 4096 -dname "cn=localhost, ou=None, L=Seattle,
ST=Washington, o=ExampleOrg, c=US" -keyalg RSA
+> keytool -genkey -alias localhost-client -keypass client-pwd -keystore
client-store -storepass client-pwd -validity 4096 -dname "cn=localhost,
ou=None, L=Seattle, ST=Washington, o=ExampleOrg, c=US" -keyalg RSA
server-store generated via
-> keytool -genkey -alias localhost -keypass server-pwd -keystore server-store
-storepass server-pwd -validity 4096 -dname "cn=localhost, ou=None, L=Seattle,
ST=Washington, o=ExampleOrg, c=US" -keyalg RSA
+> keytool -genkey -alias localhost-server -keypass server-pwd -keystore
server-store -storepass server-pwd -validity 4096 -dname "cn=localhost,
ou=None, L=Seattle, ST=Washington, o=ExampleOrg, c=US" -keyalg RSA
+
+The server key has to have name "localhost" otherwise the hostname matching
will fail (as the server is running on localhost as well).
+Both certificates are self-signed i.e. have themselves as issuer and contain
both public and private keys!
+Although the latter is not necessary for trust stores it won't do any harm
here.
diff --git a/maven-resolver-test-http/src/main/resources/ssl/client-store
b/maven-resolver-test-http/src/main/resources/ssl/client-store
index fbfb39d82..f8fe6bd76 100644
Binary files a/maven-resolver-test-http/src/main/resources/ssl/client-store and
b/maven-resolver-test-http/src/main/resources/ssl/client-store differ
diff --git a/maven-resolver-test-http/src/main/resources/ssl/server-store
b/maven-resolver-test-http/src/main/resources/ssl/server-store
index 6137fee7b..61dcd8217 100644
Binary files a/maven-resolver-test-http/src/main/resources/ssl/server-store and
b/maven-resolver-test-http/src/main/resources/ssl/server-store differ
diff --git
a/maven-resolver-test-http/src/main/resources/ssl/server-store-selfsigned
b/maven-resolver-test-http/src/main/resources/ssl/server-store-selfsigned
deleted file mode 100644
index caf6e52be..000000000
Binary files
a/maven-resolver-test-http/src/main/resources/ssl/server-store-selfsigned and
/dev/null differ
diff --git a/maven-resolver-tools/pom.xml b/maven-resolver-tools/pom.xml
index 677b73a59..d1ebde2bb 100644
--- a/maven-resolver-tools/pom.xml
+++ b/maven-resolver-tools/pom.xml
@@ -170,6 +170,25 @@
<build>
<plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-enforcer-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>enforce-bytecode-version</id>
+ <configuration>
+ <rules>
+ <enforceBytecodeVersion>
+ <excludes>
+ <!-- this requires Java 22 but is optional only -->
+
<exclude>org.eclipse.jetty.quic:jetty-quic-quiche-foreign</exclude>
+ </excludes>
+ </enforceBytecodeVersion>
+ </rules>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
diff --git a/maven-resolver-tools/src/main/resources/configuration.md.vm
b/maven-resolver-tools/src/main/resources/configuration.md.vm
index 2b5669e89..9efd0a5ae 100644
--- a/maven-resolver-tools/src/main/resources/configuration.md.vm
+++ b/maven-resolver-tools/src/main/resources/configuration.md.vm
@@ -68,6 +68,7 @@ From | To | With
`String` | `int` |
[`Integer.parseInt(...)`](https://docs.oracle.com/javase/7/docs/api/java/lang/Integer.html#parseInt(java.lang.String))
`String` | `long` |
[`Long.parseLong(...)`](https://docs.oracle.com/javase/7/docs/api/java/lang/Long.html#parseLong(java.lang.String))
`String` | `float` |
[`Float.parseFloat(...)`](https://docs.oracle.com/javase/7/docs/api/java/lang/Float.html#parseFloat(java.lang.String))
+`String` | `T extends Enum<T>` |
[`Enum.valueOf(...)`](https://docs.oracle.com/javase/7/docs/api/java/lang/Enum.html#valueOf(java.lang.Class,%20java.lang.String))
## Set Configuration from Apache Maven
diff --git
a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java
b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java
index c9ff11168..41f43701b 100644
---
a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java
+++
b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java
@@ -67,6 +67,9 @@ public class ApacheRFC9457Reporter extends
RFC9457Reporter<CloseableHttpResponse
@Override
protected String getBody(final CloseableHttpResponse response) throws
IOException {
+ if (response.getEntity() == null) {
+ return "";
+ }
return EntityUtils.toString(response.getEntity(),
StandardCharsets.UTF_8);
}
}
diff --git
a/maven-resolver-transport-apache/src/test/java/org/eclipse/aether/transport/apache/ApacheTransporterTest.java
b/maven-resolver-transport-apache/src/test/java/org/eclipse/aether/transport/apache/ApacheTransporterTest.java
index d08168773..ec5965c12 100644
---
a/maven-resolver-transport-apache/src/test/java/org/eclipse/aether/transport/apache/ApacheTransporterTest.java
+++
b/maven-resolver-transport-apache/src/test/java/org/eclipse/aether/transport/apache/ApacheTransporterTest.java
@@ -33,10 +33,10 @@ import
org.eclipse.aether.internal.test.util.http.RecordingTransportListener;
import org.eclipse.aether.spi.connector.transport.GetTask;
import org.eclipse.aether.spi.connector.transport.PutTask;
import org.eclipse.aether.spi.io.PathProcessorSupport;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Apache Transporter UT.
@@ -54,9 +54,14 @@ class ApacheTransporterTest extends HttpTransporterTest {
}
@Override
- @Disabled
- @Test
- protected void testGet_HTTPS_HTTP2Only_Insecure_SecurityMode() throws
Exception {}
+ protected boolean supportsHttp3() {
+ return false;
+ }
+
+ @Override
+ protected boolean supportsHttp2() {
+ return false;
+ }
@Test
void testGet_WebDav() throws Exception {
@@ -106,7 +111,7 @@ class ApacheTransporterTest extends HttpTransporterTest {
@Test
void testConnectionReuse() throws Exception {
- httpServer.addSslConnector();
+ httpServer.addHttp2ConnectorWithMutualTLS();
session.setCache(new DefaultRepositoryCache());
for (int i = 0; i < 3; i++) {
newTransporter(httpServer.getHttpsUrl());
@@ -122,7 +127,7 @@ class ApacheTransporterTest extends HttpTransporterTest {
@Test
void testConnectionNoReuse() throws Exception {
- httpServer.addSslConnector();
+ httpServer.addHttp2ConnectorWithMutualTLS();
session.setCache(new DefaultRepositoryCache());
session.setConfigProperty(ConfigurationProperties.HTTP_REUSE_CONNECTIONS,
false);
for (int i = 0; i < 3; i++) {
diff --git
a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/pom.xml
b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/pom.xml
index 2a9369034..12e3cc8dc 100644
--- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/pom.xml
+++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/pom.xml
@@ -74,6 +74,12 @@
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>jul-to-slf4j</artifactId>
+ <version>${slf4jVersion}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-test-util</artifactId>
diff --git
a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
index 0cb866af9..c53f7fcf3 100644
---
a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
+++
b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
@@ -44,6 +44,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.net.http.HttpClient;
+import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
@@ -71,6 +72,7 @@ import com.github.mizosoft.methanol.Methanol;
import com.github.mizosoft.methanol.RetryInterceptor;
import com.github.mizosoft.methanol.RetryInterceptor.Context;
import org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.ConfigurationProperties.HttpVersion;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.AuthenticationContext;
import org.eclipse.aether.repository.RemoteRepository;
@@ -105,7 +107,6 @@ import static
org.eclipse.aether.spi.connector.transport.http.HttpConstants.RANG
import static
org.eclipse.aether.spi.connector.transport.http.HttpConstants.USER_AGENT;
import static
org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.CONFIG_PROP_HTTP_VERSION;
import static
org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.CONFIG_PROP_MAX_CONCURRENT_REQUESTS;
-import static
org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.DEFAULT_HTTP_VERSION;
import static
org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.DEFAULT_MAX_CONCURRENT_REQUESTS;
/**
@@ -468,6 +469,52 @@ final class JdkTransporter extends AbstractTransporter
implements HttpTransporte
}
}
+ HttpClient.Version getHttpVersion(RepositorySystemSession session,
RemoteRepository repository) {
+ HttpVersion httpVersion = HttpTransporterUtils.getHttpVersion(session,
repository);
+ if (httpVersion == ConfigurationProperties.DEFAULT_HTTP_VERSION) {
+ // Fall back to legacy JDK Transporter specific property when it
is explicitly configured.
+ String configuredLegacyHttpVersion = ConfigUtils.getString(
+ session, null, CONFIG_PROP_HTTP_VERSION + "." +
repository.getId(), CONFIG_PROP_HTTP_VERSION);
+ if (configuredLegacyHttpVersion != null) {
+ return resolveHttpVersion(configuredLegacyHttpVersion);
+ }
+ return HttpClient.Version.HTTP_2;
+ } else {
+ switch (httpVersion) {
+ case MAXIMUM:
+ return getMaximumSupportedHttpVersion();
+ case HTTP_1_1:
+ return HttpClient.Version.HTTP_1_1;
+ case HTTP_2:
+ case DEFAULT:
+ return HttpClient.Version.HTTP_2;
+ case HTTP_3:
+ return resolveHttpVersion("HTTP_3");
+ default:
+ // unreachable but necessary for Checkstyle to not
complain about missing default case
+ throw new IllegalStateException("Unknown HTTP version: " +
httpVersion);
+ }
+ }
+ }
+
+ private HttpClient.Version resolveHttpVersion(String requestedVersion) {
+ try {
+ return HttpClient.Version.valueOf(requestedVersion);
+ } catch (IllegalArgumentException e) {
+ HttpClient.Version maximumHttpVersion =
getMaximumSupportedHttpVersion();
+ LOGGER.warn(
+ "HTTP version '{}' is not supported by the running JRE,
using '{}' instead",
+ requestedVersion,
+ maximumHttpVersion);
+ return maximumHttpVersion;
+ }
+ }
+
+ HttpClient.Version getMaximumSupportedHttpVersion() {
+ HttpClient.Version[] values = HttpClient.Version.values();
+ return values[values.length - 1];
+ }
+
private HttpClient createClient(RepositorySystemSession session,
RemoteRepository repository, boolean insecure)
throws RuntimeException {
@@ -484,9 +531,18 @@ final class JdkTransporter extends AbstractTransporter
implements HttpTransporte
}
}
+ Version httpVersion = getHttpVersion(session, repository);
if (sslContext == null) {
try {
if (insecure) {
+ if (httpVersion.name().equals("HTTP_3")) {
+ // custom trust manager not supported for HTTP/3 (Quic)
+ //
(https://github.com/openjdk/jdk/blob/631b675d7949a0e6312d8d6f45e2515d53b12f05/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java#L529)
+ // https://openjdk.org/jeps/517
+ //
https://mail.openjdk.org/archives/list/[email protected]/thread/LHSC7MWRFDJE2KGS2QMPFJPZX3XEQKOQ/
+ throw new IllegalStateException(
+ "Insecure HTTPS connections are not supported
for HTTP/3 (Quic)");
+ }
sslContext = SSLContext.getInstance("TLS");
X509ExtendedTrustManager tm = new
X509ExtendedTrustManager() {
@Override
@@ -523,14 +579,15 @@ final class JdkTransporter extends AbstractTransporter
implements HttpTransporte
throw new IllegalStateException("SSL Context setup
failure", e);
}
}
+ } else {
+ if (insecure) {
+ throw new IllegalStateException(
+ "Insecure HTTPS connections are not supported when a
custom SSLContext is configured");
+ }
}
Methanol.Builder builder = Methanol.newBuilder()
- .version(HttpClient.Version.valueOf(ConfigUtils.getString(
- session,
- DEFAULT_HTTP_VERSION,
- CONFIG_PROP_HTTP_VERSION + "." + repository.getId(),
- CONFIG_PROP_HTTP_VERSION)))
+ .version(httpVersion)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofMillis(connectTimeout))
// this only considers the time until the response header is
received, see
diff --git
a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java
b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java
index 78b8a4dcb..b3550af61 100644
---
a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java
+++
b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java
@@ -21,8 +21,9 @@ package org.eclipse.aether.transport.jdk;
import java.net.http.HttpClient;
/**
- * JDK Transport that properly closes {@link HttpClient} on Java 11-20.
+ * JDK Transport that properly closes {@link HttpClient} on Java 21+.
*
+ * @see <a
href="https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpClient.html#closing">HttpClient
Closing</a>
* @since 2.0.0
*/
public final class JdkTransporterCloser {
diff --git
a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterConfigurationKeys.java
b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterConfigurationKeys.java
index 83b72fa3d..bee35ed72 100644
---
a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterConfigurationKeys.java
+++
b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterConfigurationKeys.java
@@ -39,7 +39,9 @@ public final class JdkTransporterConfigurationKeys {
* @configurationType {@link java.lang.String}
* @configurationDefaultValue {@link #DEFAULT_HTTP_VERSION}
* @configurationRepoIdSuffix Yes
+ * @deprecated Use {@link ConfigurationProperties#HTTP_VERSION} instead.
This property is kept for backward compatibility and will be removed in future
versions.
*/
+ @Deprecated
public static final String CONFIG_PROP_HTTP_VERSION = CONFIG_PROPS_PREFIX
+ "httpVersion";
public static final String DEFAULT_HTTP_VERSION = "HTTP_1_1";
diff --git
a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java
b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java
index c570fa5b0..73f86fc90 100644
---
a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java
+++
b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java
@@ -20,8 +20,11 @@ package org.eclipse.aether.transport.jdk;
import java.net.ConnectException;
import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Version;
import java.util.stream.Stream;
+import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.internal.impl.DefaultPathProcessor;
import org.eclipse.aether.internal.test.util.TestUtils;
import org.eclipse.aether.internal.test.util.http.HttpTransporterTest;
@@ -30,6 +33,7 @@ import org.eclipse.aether.spi.connector.transport.PeekTask;
import org.eclipse.aether.spi.connector.transport.Transporter;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -42,6 +46,14 @@ import static org.junit.jupiter.api.Assertions.fail;
*/
class JdkTransporterTest extends HttpTransporterTest {
+ static {
+ // uncomment to enable http client logging
+ //
https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/module-summary.html#jdk.httpclient.HttpClient.log
+ // System.setProperty("jdk.httpclient.HttpClient.log", "all");
+ // even more fine-grained logging
+ // System.setProperty("jdk.internal.httpclient.debug", "true");
+ }
+
private boolean isBetweenJava17and21() {
JRE currentJre = JRE.currentJre();
return currentJre.compareTo(JRE.JAVA_17) >= 0 &&
currentJre.compareTo(JRE.JAVA_21) <= 0;
@@ -53,11 +65,25 @@ class JdkTransporterTest extends HttpTransporterTest {
return !isBetweenJava17and21();
}
+ @Override
+ protected boolean supportsHttp3() {
+ JRE currentJre = JRE.currentJre();
+ return currentJre.compareTo(JRE.JAVA_26) >= 0;
+ }
+
@Override
protected Stream<String> supportedCompressionAlgorithms() {
return Stream.of("gzip", "deflate");
}
+ @Override
+ @Disabled
+ @Test
+ protected void testPut_SSL() throws Exception {
+ // fails due to 500 status being returned by Jetty
+ //
(https://github.com/jetty/jetty.project/issues/13157#issuecomment-4980954802).
+ }
+
@Override
@Disabled
@Test
@@ -80,12 +106,14 @@ class JdkTransporterTest extends HttpTransporterTest {
@Override
@Test
+ @EnabledForJreRange(max = JRE.JAVA_25)
protected void testRetryHandler_defaultCount_negative() throws Exception {
// internally JDK client uses its own retry mechanism with
- // 2 attempts for ConnectionExpiredException (prior Java 26) and 5
(default for
- // system property "jdk.httpclient.redirects.retrylimit") attempts
after,
- // therefore explicitly limit the internal retry count to 1
- System.setProperty("jdk.httpclient.redirects.retrylimit", "1"); //
this only affects Java 26+
+ // 2 attempts for ConnectionExpiredException (prior Java 26)
+ // afterwards it uses 5 (default for
+ // system property "jdk.httpclient.redirects.retrylimit") attempts,
+ // limit the internal retry count to 1 has global effect and is too
late here anyway therefore we skip this test
+ // for Java 26+.
// Compare with https://github.com/mizosoft/methanol/issues/174
httpServer.setConnectionsToClose(8);
try {
@@ -99,11 +127,6 @@ class JdkTransporterTest extends HttpTransporterTest {
super(() -> new JdkTransporterFactory(standardChecksumExtractor(), new
DefaultPathProcessor()));
}
- @Override
- protected void enableHttp2Protocol() {
-
session.setConfigProperty(JdkTransporterConfigurationKeys.CONFIG_PROP_HTTP_VERSION,
"HTTP_2");
- }
-
@Test
void enhanceConnectExceptionMessages() {
String uri = "https://localhost:12345/";
@@ -126,4 +149,21 @@ class JdkTransporterTest extends HttpTransporterTest {
"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
JdkTransporter.getBasicAuthValue("Aladdin", "open
sesame".toCharArray()));
}
+
+ @Test
+ void testMaximumHttpVersionAtRuntime() throws Exception {
+ RepositorySystemSession session = TestUtils.newSession();
+ RemoteRepository remoteRepository =
+ new RemoteRepository.Builder("central", "default",
"https://repo.maven.apache.org/maven2/").build();
+ JdkTransporterFactory factory = new JdkTransporterFactory(s -> null,
new DefaultPathProcessor());
+
+ try (Transporter transporter = factory.newInstance(session,
remoteRepository)) {
+ JdkTransporter jdkTransporter = (JdkTransporter) transporter;
+ HttpClient.Version[] versions = HttpClient.Version.values();
+ HttpClient.Version maximumSupported = versions[versions.length -
1];
+ assertEquals(maximumSupported,
jdkTransporter.getMaximumSupportedHttpVersion());
+ // default should be returned which is HTTP_2 for all JRE versions
+ assertEquals(Version.HTTP_2,
jdkTransporter.getHttpVersion(session, remoteRepository));
+ }
+ }
}
diff --git a/maven-resolver-transport-jetty/pom.xml
b/maven-resolver-transport-jetty/pom.xml
index 87f845454..08e1cabe3 100644
--- a/maven-resolver-transport-jetty/pom.xml
+++ b/maven-resolver-transport-jetty/pom.xml
@@ -72,6 +72,18 @@
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-http2-client-transport</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http3</groupId>
+ <artifactId>jetty-http3-client-transport</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.quic</groupId>
+ <artifactId>jetty-quic-quiche-client</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.quic</groupId>
+ <artifactId>jetty-quic-quiche-jna</artifactId>
+ </dependency>
<!-- supported compressions -->
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
@@ -114,6 +126,25 @@
<build>
<plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-enforcer-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>enforce-bytecode-version</id>
+ <configuration>
+ <rules>
+ <enforceBytecodeVersion>
+ <excludes>
+ <!-- this requires Java 22 but is optional only -->
+
<exclude>org.eclipse.jetty.quic:jetty-quic-quiche-foreign</exclude>
+ </excludes>
+ </enforceBytecodeVersion>
+ </rules>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
<plugin>
<groupId>org.eclipse.sisu</groupId>
<artifactId>sisu-maven-plugin</artifactId>
diff --git
a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
index 1f3ead29e..a1b594c6d 100644
---
a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
+++
b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
@@ -19,7 +19,6 @@
package org.eclipse.aether.transport.jetty;
import javax.net.ssl.SSLContext;
-import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
@@ -28,7 +27,9 @@ import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
-import java.security.cert.X509Certificate;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
@@ -39,6 +40,7 @@ import java.util.function.Function;
import java.util.regex.Matcher;
import org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.ConfigurationProperties.HttpVersion;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.AuthenticationContext;
import org.eclipse.aether.repository.RemoteRepository;
@@ -67,7 +69,13 @@ import
org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http2.client.HTTP2Client;
import
org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2;
+import org.eclipse.jetty.http3.client.HTTP3Client;
+import org.eclipse.jetty.http3.client.HTTP3ClientQuicConfiguration;
+import
org.eclipse.jetty.http3.client.transport.ClientConnectionFactoryOverHTTP3;
+import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
+import org.eclipse.jetty.quic.quiche.client.QuicheClientQuicConfiguration;
+import org.eclipse.jetty.quic.quiche.client.QuicheTransport;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import static
org.eclipse.aether.spi.connector.transport.http.HttpConstants.ACCEPT_ENCODING;
@@ -388,58 +396,62 @@ final class JettyTransporter extends AbstractTransporter
implements HttpTranspor
}
}
- if (sslContext == null) {
+ SslContextFactory.Client sslContextFactory = new
SslContextFactory.Client();
+ if (insecure) {
+ // this is also passed on to Quiche for HTTP/3
+ sslContextFactory.setEndpointIdentificationAlgorithm(null);
+ sslContextFactory.setHostnameVerifier((name, context) -> true);
+ sslContextFactory.setTrustAll(true);
+ } else {
try {
- if (insecure) {
- sslContext = SSLContext.getInstance("TLS");
- X509TrustManager tm = new X509TrustManager() {
- @Override
- public void checkClientTrusted(X509Certificate[]
chain, String authType) {}
-
- @Override
- public void checkServerTrusted(X509Certificate[]
chain, String authType) {}
-
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[0];
- }
- };
- sslContext.init(null, new X509TrustManager[] {tm}, null);
- } else {
+ if (sslContext == null) {
+ // use the JVM's default SSL context (potentially with
custom keystores/truststores)
+ // https://github.com/jetty/jetty.project/issues/15378
sslContext = SSLContext.getDefault();
}
- } catch (Exception e) {
- if (e instanceof RuntimeException) {
- throw (RuntimeException) e;
- } else {
- throw new IllegalStateException("SSL Context setup
failure", e);
- }
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("SSL Context setup failure",
e);
}
}
- SslContextFactory.Client sslContextFactory = new
SslContextFactory.Client();
- sslContextFactory.setSslContext(sslContext);
- if (insecure) {
- sslContextFactory.setEndpointIdentificationAlgorithm(null);
- sslContextFactory.setHostnameVerifier((name, context) -> true);
+ if (sslContext != null) {
+ // not properly supported by Quiche for HTTP/3, but Jetty will use
it for HTTP/2 and HTTP/1.1
+ sslContextFactory.setSslContext(sslContext);
}
ClientConnector clientConnector = new ClientConnector();
clientConnector.setSslContextFactory(sslContextFactory);
- HTTP2Client http2Client = new HTTP2Client(clientConnector);
- ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new
ClientConnectionFactoryOverHTTP2.HTTP2(http2Client);
-
- HttpClientTransportDynamic transport;
+ Collection<ClientConnectionFactory.Info> connectors = new
ArrayList<>();
if ("https".equalsIgnoreCase(repository.getProtocol())) {
- transport = new HttpClientTransportDynamic(
- clientConnector, http2,
HttpClientConnectionFactory.HTTP11); // HTTPS, prefer H2
- } else {
- transport = new HttpClientTransportDynamic(
- clientConnector, HttpClientConnectionFactory.HTTP11,
http2); // plaintext HTTP, H2 cannot be used
+ HttpVersion httpVersion =
HttpTransporterUtils.getHttpVersion(session, repository);
+ switch (httpVersion) {
+ case MAXIMUM:
+ case HTTP_3:
+ QuicheClientQuicConfiguration clientQuicConfig =
+ HTTP3ClientQuicConfiguration.configure(new
QuicheClientQuicConfiguration());
+ HTTP3Client http3Client = new
HTTP3Client(clientQuicConfig, clientConnector);
+ QuicheTransport transport = new
QuicheTransport(clientQuicConfig);
+ ClientConnectionFactoryOverHTTP3.HTTP3 http3 =
+ new
ClientConnectionFactoryOverHTTP3.HTTP3(http3Client, transport);
+ connectors.add(http3);
+ break; // fallback to HTTP/2 not supported yet
(https://github.com/jetty/jetty.project/issues/15423)
+ case HTTP_2:
+ case DEFAULT:
+ HTTP2Client http2Client = new HTTP2Client(clientConnector);
+ ClientConnectionFactoryOverHTTP2.HTTP2 http2 =
+ new
ClientConnectionFactoryOverHTTP2.HTTP2(http2Client);
+ connectors.add(http2);
+ break;
+ default:
+ break;
+ }
}
+ connectors.add(HttpClientConnectionFactory.HTTP11); // HTTP/1.1,
always supported but has least priority
- HttpClient httpClient = new HttpClient(transport);
+ HttpClientTransportDynamic dynamicTransport = new
HttpClientTransportDynamic(
+ clientConnector, connectors.toArray(new
ClientConnectionFactory.Info[0]));
+ HttpClient httpClient = new HttpClient(dynamicTransport);
httpClient.setConnectTimeout(connectTimeout);
httpClient.setIdleTimeout(requestTimeout);
httpClient.setFollowRedirects(ConfigUtils.getBoolean(
diff --git
a/maven-resolver-transport-jetty/src/test/java/org/eclipse/aether/transport/jetty/JettyTransporterTest.java
b/maven-resolver-transport-jetty/src/test/java/org/eclipse/aether/transport/jetty/JettyTransporterTest.java
index 4917ad734..fd2318f4e 100644
---
a/maven-resolver-transport-jetty/src/test/java/org/eclipse/aether/transport/jetty/JettyTransporterTest.java
+++
b/maven-resolver-transport-jetty/src/test/java/org/eclipse/aether/transport/jetty/JettyTransporterTest.java
@@ -20,6 +20,7 @@ package org.eclipse.aether.transport.jetty;
import java.util.stream.Stream;
+import org.eclipse.aether.ConfigurationProperties;
import org.eclipse.aether.internal.test.util.http.HttpTransporterTest;
import org.eclipse.aether.spi.io.PathProcessorSupport;
import org.junit.jupiter.api.Disabled;
@@ -75,6 +76,33 @@ class JettyTransporterTest extends HttpTransporterTest {
@Test
protected void
testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() {}
+ @Override
+ @Disabled
+ @Test
+ protected void testGet_HTTP3FallbackToHTTP2() throws Exception {
+ // Jetty does not support version discovery:
https://github.com/jetty/jetty.project/issues/15423
+ }
+
+ @Override
+ @Test
+ protected void testGet_HTTP3Only() throws Exception {
+ // Jetty's HTTP/3 support is based on Quiche which does not consider
the default SSL context
+ // (https://github.com/jetty/jetty.project/issues/15370)
+ session.setConfigProperty(
+ ConfigurationProperties.HTTPS_SECURITY_MODE,
ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
+ super.testGet_HTTP3Only();
+ }
+
+ @Override
+ @Test
+ protected void testGet_HTTP3() throws Exception {
+ // Jetty's HTTP/3 support is based on Quiche which does not consider
the default SSL context
+ // (https://github.com/jetty/jetty.project/issues/15370)
+ session.setConfigProperty(
+ ConfigurationProperties.HTTPS_SECURITY_MODE,
ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
+ super.testGet_HTTP3();
+ }
+
public JettyTransporterTest() {
super(() -> new JettyTransporterFactory(standardChecksumExtractor(),
new PathProcessorSupport()));
}
diff --git
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
index 523ffba14..bd8376f28 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
@@ -344,6 +344,50 @@ public final class ConfigUtils {
return getMap(session.getConfigProperties(), defaultValue, keys);
}
+ /**
+ * Gets the specified configuration property.
+ * @param <T> the enum type
+ *
+ * @param properties the configuration properties to read, must not be
{@code null}
+ * @param enumClass the enum class to read, must not be {@code null}
+ * @param defaultValue the default value to return in case none of the
property keys is set to a convertible String or T.
+ * @param keys the property keys to read, must not be {@code null}. The
specified keys are read one after one until
+ * a enum type {@code T} or a string (parsed using {@link
Enum#valueOf(Class, String)} is found.
+ * @return the property value or {@code defaultValue} if none found
+ * @since NEXT
+ */
+ public static <T extends Enum<T>> T getEnum(
+ Map<?, ?> properties, Class<T> enumClass, T defaultValue,
String... keys) {
+ for (String key : keys) {
+ Object value = properties.get(key);
+
+ if (enumClass.isInstance(value)) { // validate type
+ return enumClass.cast(value);
+ } else if (value instanceof String) {
+ return Enum.valueOf(enumClass, (String) value);
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the specified configuration property.
+ * @param <T> the enum type
+ *
+ * @param session the repository system session from which to read the
configuration property, must not be
+ * {@code null}
+ * @param enumClass the enum class to read, must not be {@code null}
+ * @param defaultValue the default value to return in case none of the
property keys is set to a convertible String or T.
+ * @param keys the property keys to read, must not be {@code null}. The
specified keys are read one after one until
+ * a enum type {@code T} or a string (parsed using {@link
Enum#valueOf(Class, String)} is found.
+ * @return the property value or {@code defaultValue} if none found
+ * @since NEXT
+ */
+ public static <T extends Enum<T>> T getEnum(
+ RepositorySystemSession session, Class<T> enumClass, T
defaultValue, String... keys) {
+ return getEnum(session.getConfigProperties(), enumClass, defaultValue,
keys);
+ }
+
/**
* Utility method to parse configuration string that contains comma
separated list of names into
* {@code List<String>}, never returns {@code null}.
diff --git
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java
index a8fe4efed..b9251e19d 100644
---
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java
+++
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java
@@ -315,6 +315,19 @@ public final class HttpTransporterUtils {
return Optional.empty();
}
+ /**
+ * Getter for {@link ConfigurationProperties#HTTP_VERSION}.
+ */
+ public static ConfigurationProperties.HttpVersion getHttpVersion(
+ RepositorySystemSession session, RemoteRepository repository) {
+ return ConfigUtils.getEnum(
+ session,
+ ConfigurationProperties.HttpVersion.class,
+ ConfigurationProperties.DEFAULT_HTTP_VERSION,
+ ConfigurationProperties.HTTP_VERSION + "." +
repository.getId(),
+ ConfigurationProperties.HTTP_VERSION);
+ }
+
/**
* Shared code to create "base {@link URI}" for most common HTTP remote
repositories and all HTTP transports.
* Note: this method just applies common validation and adjustments to
URI, but it does not enforce protocol
diff --git
a/maven-resolver-util/src/test/java/org/eclipse/aether/util/ConfigUtilsTest.java
b/maven-resolver-util/src/test/java/org/eclipse/aether/util/ConfigUtilsTest.java
index d3797a314..480fbafc8 100644
---
a/maven-resolver-util/src/test/java/org/eclipse/aether/util/ConfigUtilsTest.java
+++
b/maven-resolver-util/src/test/java/org/eclipse/aether/util/ConfigUtilsTest.java
@@ -199,4 +199,29 @@ public class ConfigUtilsTest {
config.put("some-number", -1234f);
assertEquals(-1234f, ConfigUtils.getFloat(config, 0, "some-number"),
0.1f);
}
+
+ private enum TestEnum {
+ FIRST,
+ SECOND
+ }
+
+ @Test
+ void testGetEnum() {
+ config.put("some-enum", TestEnum.SECOND);
+ assertEquals(TestEnum.SECOND, ConfigUtils.getEnum(config,
TestEnum.class, TestEnum.FIRST, "some-enum"));
+ }
+
+ @Test
+ void testGetEnum_StringConversion() {
+ config.put("some-enum", "SECOND");
+ assertEquals(TestEnum.SECOND, ConfigUtils.getEnum(config,
TestEnum.class, TestEnum.FIRST, "some-enum"));
+ }
+
+ @Test
+ void testGetEnum_StringInvalidValue() {
+ config.put("some-enum", "second");
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ConfigUtils.getEnum(config, TestEnum.class,
TestEnum.FIRST, "some-enum"));
+ }
}
diff --git a/src/site/markdown/transporter-known-issues.md
b/src/site/markdown/transporter-known-issues.md
index c83d6eaa1..43f50cf82 100644
--- a/src/site/markdown/transporter-known-issues.md
+++ b/src/site/markdown/transporter-known-issues.md
@@ -25,7 +25,8 @@ This page lists known issues related to various transporters.
Given this transporter uses the Java HttpClient (available since Java 11), it
is the user's best interest
to use latest patch version of Java, as HttpClient is getting bugfixes
regularly.
-Known issues:
+### Known issues:
+
* Does not properly support `aether.transport.http.requestTimeout`
configuration prior Java 26, see
[JDK-8208693](https://bugs.openjdk.org/browse/JDK-8208693)
* No TLS Proxy support, see
[here](https://dev.to/kdrakon/httpclient-can-t-connect-to-a-tls-proxy-118a)
* No SOCKS proxy support, see
[JDK-8214516](https://bugs.openjdk.org/browse/JDK-8214516)
@@ -34,19 +35,35 @@ Known issues:
Basic authentication disabled, see
[here](https://www.oracle.com/java/technologies/javase/8u111-relnotes.html). To
enable HTTP Basic authentication for Proxy TLS tunneling, one must set
`jdk.http.auth.tunneling.disabledScheme` to
empty string, e.g. by adding `-Djdk.http.auth.tunneling.disabledScheme=""`
JVM argument.
+* Preemptive basic authentication is only supported on Java 16 and below and
on Java 24 and above, see
[JDK-8326949](https://bugs.openjdk.org/browse/JDK-8326949).
+* HTTP/2 GOAWAY frames incorrectly handled in Java < 17.0.17, Java 18 till
Java < 21.0.8 and Java 22 till Java < 24, see
[JDK-8335181](https://bugs.openjdk.org/browse/JDK-8335181).
+* HTTP/2 GOAWAY does not lead to proper connection shutdown, see
[JDK-8385131](https://bugs.openjdk.org/browse/JDK-8385131).
+* HTTP/3 is only supported with [Java 26 and
above](https://inside.java/2025/10/22/http3-support/).
Maven 4 uses this transport by default for HTTP(S) protocol.
## The `apache` (Apache HttpClient) Transporter
-Transporter based on Apache HttpClient.
+Transporter based on Apache HttpClient 4.
To use this transporter in Maven 4, you need to specify
`-Dmaven.resolver.transport=apache` user property.
+### Known issues:
+
+* Does neither support HTTP/2 nor HTTP/3
+
## The `jetty` (Jetty HttpClient) Transporter
-Transporter based on Jetty HttpClient.
+Transporter based on Jetty HttpClient. It requires Java 17 or above.
In Maven 4 this transport is not available by default (is not bundled). To use
it,
you need to add `org.apache.maven.resolver.transport:transport-http-jetty`
artifact with its runtime dependencies to
`/lib` directory of Maven. Once added to core classpath, it will take over the
role of default transport.
+
+### Known issues:
+
+* Leveraging HTTP/3 with Jetty suffers from [poor performance due to usage of
the native Quiche Rust library]
+
(https://github.com/jetty/jetty.project/discussions/13469#discussioncomment-14125855).
This situation is
+ probably improving with Jetty 13 (which is supposed to be shipping with a
Java QUIC implementation).
+* When HTTP/3 is configured there is [no fallback to lower versions]
+ (https://github.com/jetty/jetty.project/issues/15423). The request will just
time out.