Repository: jhy/jsoup
Pull request #2340 (2025-06-18 04:15:06)

Enable HTTP/2 by default if the HttpClient is available
Change the default to prefer using the JDK's HttpClient over HttpUrlConnection, if available, to enable HTTP/2 support.

Users can disable via `-Djsoup.useHttpClient=false`.


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 7a871390..ef475dec 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -15,7 +15,8 @@
 * Added `NodeVisitor#traverse(Node)` to simplify node traversal calls (vs. importing `NodeTraversor`).
 * The HTML parser now allows the specific text-data type (Data, RcData) to be customized for known tags. (Previously, that was only supported on custom tags.) [#2326](https://github.com/jhy/jsoup/issues/2326).
 * Added `Connection#readFully()` as a replacement for `Connection#bufferUp()` with an explicit IOException. Similarly, added `Connection#readBody()` over `Connection#body()`. Deprecated `Connection#bufferUp()`. [#2327](https://github.com/jhy/jsoup/pull/2327) 
-* When serializing HTML, the `<` and `>` characters are now escaped in attributes. This helps prevent a class of mutation XSS attacks. [#2337](https://github.com/jhy/jsoup/pull/2337) 
+* When serializing HTML, the `<` and `>` characters are now escaped in attributes. This helps prevent a class of mutation XSS attacks. [#2337](https://github.com/jhy/jsoup/pull/2337)
+* Changed `Connection` to prefer using the JDK's HttpClient over HttpUrlConnection, if available, to enable HTTP/2 support by default. Users can disable via `-Djsoup.useHttpClient=false`. [#2340](https://github.com/jhy/jsoup/pull/2340)
 
 ### Bug Fixes
 * The contents of a `script` in a `svg` foreign context should be parsed as script data, not text. [#2320](https://github.com/jhy/jsoup/issues/2320)
diff --git a/src/main/java/org/jsoup/Connection.java b/src/main/java/org/jsoup/Connection.java
index 09065459..6661fde1 100644
--- a/src/main/java/org/jsoup/Connection.java
+++ b/src/main/java/org/jsoup/Connection.java
@@ -39,6 +39,9 @@ import java.util.Map;
 #execute()}, {@link #get()}, or {@link #post()}), and the server's response consumed.</p>
  <p>For multi-threaded implementations, it is important to use a {@link #newRequest()} for each request. The session may
  be shared across concurrent threads, but a not a specific request.</p>
+ <p><b>HTTP/2</b> support: On JDK/JRE 11 and above, requests use {@link java.net.http.HttpClient}, which supports
+ HTTP/2. To use the legacy {@link java.net.HttpURLConnection} instead, set
+ <code>System.setProperty("jsoup.useHttpClient", "false")</code>.</p>
  */
 @SuppressWarnings("unused")
 public interface Connection {
diff --git a/src/main/java/org/jsoup/helper/RequestDispatch.java b/src/main/java/org/jsoup/helper/RequestDispatch.java
index a1696cf4..fc73e7c9 100644
--- a/src/main/java/org/jsoup/helper/RequestDispatch.java
+++ b/src/main/java/org/jsoup/helper/RequestDispatch.java
@@ -9,9 +9,9 @@ import static org.jsoup.helper.HttpConnection.Response;
 import java.lang.reflect.Constructor;
 
 /**
- Dispatches requests to either HttpClient (JDK 11+) or HttpURLConnection implementations. At startup, if we
- can instantiate the HttpClientExecutor class, requests will use that if the system property
- {@link SharedConstants#UseHttpClient} is set to {@code true}.
+ Handles requests using either HttpClient (available in JDK 11+) or HttpURLConnection. During initialization, the
+ HttpClientExecutor class is used if it can be instantiated, unless the system property
+ {@link SharedConstants#UseHttpClient} is explicitly set to {@code false}.
  */
 class RequestDispatch {
 
@@ -31,7 +31,8 @@ class RequestDispatch {
     }
 
     static RequestExecutor get(Request request, @Nullable Response previousResponse) {
-        if (Boolean.getBoolean(SharedConstants.UseHttpClient) && clientConstructor != null) {
+        boolean useHttpClient = Boolean.parseBoolean(System.getProperty(SharedConstants.UseHttpClient, "true"));
+        if (useHttpClient && clientConstructor != null) {
             try {
                 return clientConstructor.newInstance(request, previousResponse);
             } catch (Exception e) {
diff --git a/src/main/java/org/jsoup/helper/UrlConnectionExecutor.java b/src/main/java/org/jsoup/helper/UrlConnectionExecutor.java
index 2499337d..d3a0beeb 100644
--- a/src/main/java/org/jsoup/helper/UrlConnectionExecutor.java
+++ b/src/main/java/org/jsoup/helper/UrlConnectionExecutor.java
@@ -17,8 +17,8 @@ import java.util.Map;
 import static org.jsoup.helper.HttpConnection.Response;
 
 /**
- Execute HTTP requests using the HttpURLConnection implementation. Currently used by default; set system property
- {@code jsoup.useHttpClient} to {@code false} to explicitly set.
+ Execute HTTP requests using the HttpURLConnection implementation. The HttpClient is used by default if available; set system property
+ {@code jsoup.useHttpClient} to {@code false} to explicitly prefer the HttpUrlConnection.
  */
 class UrlConnectionExecutor extends RequestExecutor {
     @Nullable
diff --git a/src/main/java11/org/jsoup/helper/HttpClientExecutor.java b/src/main/java11/org/jsoup/helper/HttpClientExecutor.java
index b7ed2955..74d02459 100644
--- a/src/main/java11/org/jsoup/helper/HttpClientExecutor.java
+++ b/src/main/java11/org/jsoup/helper/HttpClientExecutor.java
@@ -24,8 +24,8 @@ import static org.jsoup.helper.HttpConnection.Response;
 import static org.jsoup.helper.HttpConnection.Response.writePost;
 
 /**
- Executes requests using the HttpClient, for http/2 support. Currently disabled by default; enable by setting system
- property {@code jsoup.useHttpClient} to {@code true}.
+ Executes requests using the HttpClient, for http/2 support. Enabled by default when available. To disable, set
+ property {@code jsoup.useHttpClient} to {@code false}.
  */
 class HttpClientExecutor extends RequestExecutor {
     // HttpClient expects proxy settings per client; we do per request, so held as a thread local. Can't do same for


Test patch:
diff --git a/src/test/java/org/jsoup/integration/ConnectIT.java b/src/test/java/org/jsoup/integration/ConnectIT.java
index e579e617..2045fb5f 100644
--- a/src/test/java/org/jsoup/integration/ConnectIT.java
+++ b/src/test/java/org/jsoup/integration/ConnectIT.java
@@ -3,11 +3,14 @@ package org.jsoup.integration;
 import org.jsoup.Connection;
 import org.jsoup.Jsoup;
 import org.jsoup.helper.DataUtil;
+import org.jsoup.integration.servlets.EchoServlet;
 import org.jsoup.integration.servlets.FileServlet;
 import org.jsoup.integration.servlets.SlowRider;
+import org.jsoup.internal.SharedConstants;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
 import org.jsoup.parser.StreamParser;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
 import java.io.BufferedInputStream;
@@ -24,6 +27,12 @@ import static org.junit.jupiter.api.Assertions.*;
  * Failsafe integration tests for Connect methods. These take a bit longer to run, so included as Integ, not Unit, tests.
  */
 public class ConnectIT {
+    @BeforeAll
+    public static void setUp() {
+        TestServer.start();
+        System.setProperty(SharedConstants.UseHttpClient, "false"); // use the default UrlConnection. See HttpClientConnectIT for other version
+    }
+
     // Slow Rider tests.
     @Test
     public void canInterruptBodyStringRead() throws InterruptedException {
diff --git a/src/test/java11/org/jsoup/helper/HttpClientExecutorTest.java b/src/test/java11/org/jsoup/helper/HttpClientExecutorTest.java
index 4bfd5a7e..475c6788 100644
--- a/src/test/java11/org/jsoup/helper/HttpClientExecutorTest.java
+++ b/src/test/java11/org/jsoup/helper/HttpClientExecutorTest.java
@@ -14,13 +14,14 @@ public class HttpClientExecutorTest {
             assertEquals("org.jsoup.helper.HttpClientExecutor", executor.getClass().getName());
             // Haven't figured out how to get Maven to allow this mjar code to be on the classpath for the surefire tests, hence not instanceof
         } finally {
-            disableHttpClient(); // reset to default off (currently)
+            disableHttpClient(); // reset to previous default for JDK8 compat tests
         }
     }
 
     @Test void getsHttpUrlConnectionByDefault() {
+        System.clearProperty(SharedConstants.UseHttpClient);
         RequestExecutor executor = RequestDispatch.get(null, null);
-        assertInstanceOf(UrlConnectionExecutor.class, executor);
+        assertEquals("org.jsoup.helper.HttpClientExecutor", executor.getClass().getName());
     }
 
     public static void enableHttpClient() {