Skip to content

Commit

Permalink
Support http/2 requests via HttpClient (#2257)
Browse files Browse the repository at this point in the history
This adds support for the Java 11+ HttpClient on systems that support it, enabling http/2 requests. On Java 8, and on Android, requests will still go via the existing HttpURLConnection.

This is currently disabled by default -- set the system property `jsoup.useHttpClient` to enable it.
  • Loading branch information
jhy authored Jan 8, 2025
1 parent 36ba3ed commit 18f87f1
Show file tree
Hide file tree
Showing 21 changed files with 608 additions and 166 deletions.
44 changes: 39 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
</signature>
<ignores>
<ignore>java.net.HttpURLConnection</ignore><!-- .setAuthenticator(java.net.Authenticator) in Java 9; only used in multirelease 9+ version -->
<ignore>java.net.http.*</ignore><!-- HttpClient in Java 11; only used in multirelease 11+ version -->
</ignores>
</configuration>
</execution>
Expand Down Expand Up @@ -119,6 +120,10 @@
<ignore>java.util.Spliterators</ignore>
<ignore>java.nio.ByteBuffer</ignore> <!-- .flip(); added in API1; possibly due to .flip previously returning Buffer, later ByteBuffer; return unused -->
<ignore>java.net.HttpURLConnection</ignore><!-- .setAuthenticator(java.net.Authenticator) in Java 9; only used in multirelease 9+ version -->
<!-- HttpClient and following in Java 11; only used in multirelease 11+ version, guarded and not on Android -->
<ignore>java.net.http.*</ignore>
<ignore>java.time.Duration</ignore>
<ignore>java.util.OptionalLong</ignore>
</ignores>
<!-- ^ Provided by https://developer.android.com/studio/write/java8-support#library-desugaring -->
</configuration>
Expand Down Expand Up @@ -372,11 +377,11 @@
</build>
</profile>

<!-- Compiles the multi-release jar when executed on JDK9+ -->
<!-- Compiles the multi-release jar when executed on JDK11+ -->
<profile>
<id>compile-multi-release</id>
<activation>
<jdk>[9,2000)</jdk>
<jdk>[11,2000)</jdk>
</activation>
<build>
<plugins>
Expand All @@ -398,22 +403,51 @@
</execution>

<execution>
<id>compile-java-9</id>
<id>compile-java-11</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>9</release>
<release>11</release>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java9</compileSourceRoot>
<compileSourceRoot>${project.basedir}/src/main/java11</compileSourceRoot>
</compileSourceRoots>
<multiReleaseOutput>true</multiReleaseOutput>
</configuration>
</execution>

<!-- Tests for multi-release jar on 11: mark tests to be compiled; then and add to class path -->
<execution>
<id>testcompile-java-11</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<release>11</release>

<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/test/java11</compileSourceRoot>
</compileSourceRoots>
</configuration>
</execution>

</executions>
</plugin>

<!-- Add the Java 11-specific test directory to the test runtime classpath -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<additionalClasspathElements>
<additionalClasspathElement>${project.build.outputDirectory}/META-INF/versions/11</additionalClasspathElement>
</additionalClasspathElements>
<useModulePath>false</useModulePath> <!-- tests use classpath -->
</configuration>
</plugin>

</plugins>
</build>
</profile>
Expand Down
9 changes: 4 additions & 5 deletions src/main/java/org/jsoup/helper/AuthenticationHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import java.lang.reflect.Constructor;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.PasswordAuthentication;

/**
Expand All @@ -13,7 +12,7 @@
ThreadLocal.
*/
class AuthenticationHandler extends Authenticator {
static final int MaxAttempts = 5; // max authentication attempts per request. allows for multiple auths (e.g. proxy and server) in one request, but saves otherwise 20 requests if credentials are incorrect.
static final int MaxAttempts = 3; // max authentication attempts per request. allows for multiple auths (e.g. proxy and server) in one request, but saves otherwise 20 requests if credentials are incorrect.
static AuthShim handler;

static {
Expand Down Expand Up @@ -50,7 +49,7 @@ class AuthenticationHandler extends Authenticator {
// it may be an interactive prompt, and the user could eventually get it right). But in Jsoup's context, the
// auth will either be correct or not, so just abandon
if (delegate.attemptCount > MaxAttempts)
return null;
return null; // When using HttpClient, this will manifest as "No credentials provided" IOException; not ideal; would be clearer if we could then detach the authenticator which would bubble the 401, but there's no path for that
if (delegate.auth == null)
return null; // detached - would have been the Global Authenticator (not a delegate)

Expand All @@ -60,7 +59,7 @@ class AuthenticationHandler extends Authenticator {
}

interface AuthShim {
void enable(RequestAuthenticator auth, HttpURLConnection con);
void enable(RequestAuthenticator auth, Object connOrHttp);

void remove();

Expand All @@ -76,7 +75,7 @@ static class GlobalHandler implements AuthShim {
Authenticator.setDefault(new AuthenticationHandler());
}

@Override public void enable(RequestAuthenticator auth, HttpURLConnection con) {
@Override public void enable(RequestAuthenticator auth, Object ignored) {
authenticators.set(new AuthenticationHandler(auth));
}

Expand Down
8 changes: 4 additions & 4 deletions src/main/java/org/jsoup/helper/CookieUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import java.io.IOException;
import java.net.CookieManager;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
Expand All @@ -19,6 +18,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;

/**
Helper functions to support the Cookie Manager / Cookie Storage in HttpConnection.
Expand All @@ -35,7 +35,7 @@ class CookieUtil {
Pre-request, get any applicable headers out of the Request cookies and the Cookie Store, and add them to the request
headers. If the Cookie Store duplicates any Request cookies (same name and value), they will be discarded.
*/
static void applyCookiesToRequest(HttpConnection.Request req, HttpURLConnection con) throws IOException {
static void applyCookiesToRequest(HttpConnection.Request req, BiConsumer<String, String> setter) throws IOException {
// Request key/val cookies. LinkedHashSet used to preserve order, as cookie store will return most specific path first
Set<String> cookieSet = requestCookieSet(req);
Set<String> cookies2 = null;
Expand All @@ -62,9 +62,9 @@ else if (Cookie2Name.equals(key)) {
}

if (cookieSet.size() > 0)
con.addRequestProperty(CookieName, StringUtil.join(cookieSet, Sep));
setter.accept(CookieName, StringUtil.join(cookieSet, Sep));
if (cookies2 != null && cookies2.size() > 0)
con.addRequestProperty(Cookie2Name, StringUtil.join(cookies2, Sep));
setter.accept(Cookie2Name, StringUtil.join(cookies2, Sep));
}

private static LinkedHashSet<String> requestCookieSet(Connection.Request req) {
Expand Down
Loading

0 comments on commit 18f87f1

Please sign in to comment.