Skip to content

8349910: Implement JEP 517: HTTP/3 for the HTTP Client API #24751

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

Open
wants to merge 525 commits into
base: master
Choose a base branch
from

Conversation

dfuch
Copy link
Member

@dfuch dfuch commented Apr 18, 2025

Hi,

Please find here a PR for the implementation of JEP 517: HTTP/3 for the HTTP Client API.

The CSR can be viewed at JDK-8350588: Implement JEP 517: HTTP/3 for the HTTP Client API

This JEP proposes to enhance the HttpClient implementation to support HTTP/3.
It adds a non-exposed / non-exported internal implementation of the QUIC protocol based on DatagramChannel and the SunJSSE SSLContext provider.


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change requires CSR request JDK-8350588 to be approved

Issues

  • JDK-8349910: Implement JEP 517: HTTP/3 for the HTTP Client API (Enhancement - P3)
  • JDK-8350588: Implement JEP 517: HTTP/3 for the HTTP Client API (CSR)

Contributors

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/24751/head:pull/24751
$ git checkout pull/24751

Update a local copy of the PR:
$ git checkout pull/24751
$ git pull https://git.openjdk.org/jdk.git pull/24751/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 24751

View PR using the GUI difftool:
$ git pr show -t 24751

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/24751.diff

Using Webrev

Link to Webrev Comment

djelinski and others added 30 commits March 21, 2025 12:07
…oesn't support HTTP/3, the request version is HTTP_3, and HTTP_ONLY is not specified
@openjdk openjdk bot removed the merge-conflict Pull request has merge conflict with target branch label Jun 26, 2025
Comment on lines +125 to +126
* @param peerHost The peer hostname or IP address. Can be null.
* @param peerPort The peer port, can be -1 if the port is unknown
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would that be the hostname in the URI, or in the AltService?
Maybe we could add an @apiNote here to clarify it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the javadoc here was written to match the one on SSLContext#createSSLEngine. The peer information is used for caching, but it's also used in the SNI extension, so ideally users should use the URI address, not the alt service one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Maybe that would deserve a note - since with HTTP/3 we have potentially two addresses and two ports.

import java.util.Optional;

/**
* Represents the Quic versions defined in their corresponding RFCs
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a note here to warn that new enum constants may be added to this enum in the future.

QUIC_V1(1), // RFC-9000
QUIC_V2(0x6b3343cf); // RFC 9369

private final int versionNumber;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could add a comment here to say that the version is defined as a 32bits unsigned number, and we chose a simple int to represent it.

if (shc.sslConfig.isQuic) {
QuicTLSEngineImpl engine =
(QuicTLSEngineImpl) shc.conContext.transport;
engine.deriveOneRTTKeys();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not derive the server's 1RTT read keys before processing the client's Finished message.

Also, we could skip calculating the SSL WriteCipher when QUIC is in use. Also, we're calculating the baseWriteSecret twice (deriveOneRTTKeys calculates the same secret)

@@ -46,7 +46,7 @@ final class KeyShareExtension {
new CHKeyShareProducer();
static final ExtensionConsumer chOnLoadConsumer =
new CHKeyShareConsumer();
static final HandshakeAbsence chOnTradAbsence =
static final HandshakeAbsence chOnTradeAbsence =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we fix that one in mainline to remove this file from the PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +159 to +168
// apply to QuicEngine only
byte[] getHandshakeMessage() {
throw new UnsupportedOperationException();
}

// apply to QuicEngine only
QuicTLSEngine.KeySpace getHandshakeMessageKeySpace() {
throw new UnsupportedOperationException();
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need those two methods here? Or could we just have them on QuicEngineOutputRecord and do the appropriate cast at the point they are called?
Or do we have that at this level for performance reasons?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done to match the surrounding code. There's a bunch of methods in OutputRecord that throw UOE and are overridden by one class only. I suppose it would be cleaner to just do the cast, but "when in Rome, do as the Romans do"

@@ -58,7 +58,7 @@ final class PreSharedKeyExtension {
new CHPreSharedKeyOnLoadAbsence();
static final HandshakeConsumer chOnTradeConsumer =
new CHPreSharedKeyUpdate();
static final HandshakeAbsence chOnTradAbsence =
static final HandshakeAbsence chOnTradeAbsence =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we bring this change to mainline to remove this file from this PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throws GeneralSecurityException {
super(cipherSuite, secret, new T13AESHPCipher(hp), keyPhase);
this.key = key;
this.iv = iv;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.iv = iv;
this.iv = iv.clone();

final int keyPhase) throws GeneralSecurityException {
super(cipherSuite, secret, new T13AESHPCipher(hp), keyPhase);
this.key = key;
this.iv = iv;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.iv = iv;
this.iv = iv.clone();

throws GeneralSecurityException {
super(cipherSuite, secret, new T13CC20HPCipher(hp), keyPhase);
this.key = key;
this.iv = iv;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.iv = iv;
this.iv = iv.clone();

Comment on lines +36 to +37
* QuicTransportParametersExtension is an implementation of RFC 9000.
* It is only used by QUIC connections.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe referencing RFC 9001 Section 8.2 would be more appropriate here.
The QUIC transport parameters themselves are defined in RFC 9000, but the use of the quic_transport_parameters extension in the ClientHello and the EncryptedExtensions messages is described in RFC 9001.
Or else the purpose and description of this class could be clarified.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the extensions have a generic JavaDoc, I will update this one to match the others.

@@ -458,6 +458,28 @@ enum SSLExtension implements SSLStringizer {
null, null, null, null,
KeyShareExtension.hrrStringizer),

// Extension defined in RFC 9000
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be RFC 9001 here too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be updated in the next refresh.

}
}
}
}

// true if previous attempt resulted in streamLimitReached
public boolean hasReachedStreamLimit(Version version) { return streamLimitReached == version; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems that streamLimitReached will either be equal to version or null. If that's the case, let's use a boolean instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point.

@@ -111,8 +120,12 @@ HttpClientImpl client() {

// Keeps track of the underlying connection when establishing an HTTP/2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is now used with HTTP/3, should we update this comment?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have verified the other comments in this class and updated to mention or HTTP/3 in a few places in this class, in my local repo. Will appear the next time I sync this PR.

@@ -47,7 +47,7 @@ final class SupportedGroupsExtension {
new CHSupportedGroupsProducer();
static final ExtensionConsumer chOnLoadConsumer =
new CHSupportedGroupsConsumer();
static final HandshakeAbsence chOnTradAbsence =
static final HandshakeAbsence chOnTradeAbsence =
Copy link
Member Author

@dfuch dfuch Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another one that could disappear if we fixed that typo in mainline first.
@djelinski would you be willing to do that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +222 to +224
// TODO in future, when QuicTLSEngine might become a public exported interface
// we should have a method on javax.net.ssl.X509ExtendedKeyManager that
// takes QuicTLSEngine.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TODO in future, when QuicTLSEngine might become a public exported interface
// we should have a method on javax.net.ssl.X509ExtendedKeyManager that
// takes QuicTLSEngine.
// TODO in future, if QuicTLSEngine ever becomes a public exported interface
// we might have a method on javax.net.ssl.X509ExtendedKeyManager that
// takes QuicTLSEngine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could stop guessing the future, and use this instead:

Suggested change
// TODO in future, when QuicTLSEngine might become a public exported interface
// we should have a method on javax.net.ssl.X509ExtendedKeyManager that
// takes QuicTLSEngine.
// TODO add a method on javax.net.ssl.X509ExtendedKeyManager that
// takes QuicTLSEngine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed speculations about the future

Comment on lines +316 to +318
// TODO in future, when QuicTLSEngine might become a public exported interface
// we should have a method on javax.net.ssl.X509ExtendedKeyManager that
// takes QuicTLSEngine.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TODO in future, when QuicTLSEngine might become a public exported interface
// we should have a method on javax.net.ssl.X509ExtendedKeyManager that
// takes QuicTLSEngine.
// TODO in future, if QuicTLSEngine ever becomes a public exported interface
// we might have a method on javax.net.ssl.X509ExtendedKeyManager that
// takes QuicTLSEngine.

Comment on lines +40 to +43
* <a href="https://tools.ietf.org/html/rfc9114">Hypertext Transfer Protocol
* Version 3 (HTTP/3)</a>, the <a href="https://tools.ietf.org/html/rfc7540">
* Hypertext Transfer Protocol Version 2 (HTTP/2)</a>, the
* <a href="https://tools.ietf.org/html/rfc2616">
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is strange to link to a different place here than in the @spec tags below. I will fix that.

Comment on lines +179 to +186
static Origin of(final String scheme, final InetSocketAddress addr)
throws IllegalArgumentException {
// we use getHostString(), since the address could be unresolved in the case where
// proxy is configured
final String originHost = addr.getHostString();
return new Origin(scheme, originHost, addr.getPort());
}
}
Copy link
Member Author

@dfuch dfuch Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's a good idea to rely on InetSocketAddress::getHostString() as this can change depending on how the InetSocketAddress was constructed and whether getHost() has been called.
I believe it would be more reliable to add a new URI origin parameter to the HttpConnection constructor. We could build that origin from the request URI for which the connection was created. Probably origin should be null for PlainProxyConnection.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created https://bugs.openjdk.org/browse/JDK-8361060 to track this.

@@ -111,8 +120,12 @@ HttpClientImpl client() {

// Keeps track of the underlying connection when establishing an HTTP/2
// exchange so that it can be aborted/timed out mid setup.
static final class ConnectionAborter {
final class ConnectionAborter {
// In case of HTTP/3 direct connection we may have
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"direct" is usually used to refer to connections that don't use a proxy; since H3 connections never use a proxy, can we drop the "direct" here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I will rephrase. We should be speaking of HTTP/3 requests (and not connection) here anyway.

// there's nothing more left to parse
return new ParsedHeaderValue(altValue, alpnName, hostPort.host(), hostPort.port(), Map.of());
}
// parse the semi-colon delimited parameters out of the rest of the remaining string
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// parse the semi-colon delimited parameters out of the rest of the remaining string
// parse the semicolon delimited parameters out of the rest of the remaining string

@@ -79,6 +81,13 @@ final class Exchange<T> {

final AtomicInteger nonFinalResponses = new AtomicInteger();

// This will be set to true only when it is guaranteed that the server hasn't processed
// the request. Typically this happens when the server explicitly states (through a GOAWAY frame
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// the request. Typically this happens when the server explicitly states (through a GOAWAY frame
// the request. Typically, this happens when the server explicitly states (through a GOAWAY frame

@@ -111,8 +120,12 @@ HttpClientImpl client() {

// Keeps track of the underlying connection when establishing an HTTP/2
// exchange so that it can be aborted/timed out mid setup.
static final class ConnectionAborter {
final class ConnectionAborter {
// In case of HTTP/3 direct connection we may have
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I will rephrase. We should be speaking of HTTP/3 requests (and not connection) here anyway.

* {@return the current partial frame, if any}
* This method doesn't try to do any decoding.
*/
public Http3Frame peek() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is unused. I'll remove it.

private volatile QuicStreamWriter localWriter;
private final CompletableFuture<QuicStreamWriter> streamWriterCF;
// a queue of ByteBuffers submitted for writing.
// might be null if not used. Only used in QueuingStreamPair.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be true. I'll see if I can move it to QueuingStreamPair.

SequentialScheduler.lockingScheduler(this::peerReaderLoop);
// The QuicStreamReader for the peer control stream
volatile QuicStreamReader peerReader;
private final CompletableFuture<QuicStreamReader> streamReaderCF;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused

* to the receiver part of this stream pair after the remote HTTP/3 stream
* type has been read off the remote initiated stream}
*/
public final CompletableFuture<QuicStreamReader> futureReceiverStreamReader() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core-libs [email protected] csr Pull request needs approved CSR before integration net [email protected] rfr Pull request is ready for review security [email protected]
Development

Successfully merging this pull request may close these issues.

9 participants