HTTP/3 Client Library

In the vast majority of cases, client applications should use the generic, high-level, HTTP client library that also provides HTTP/3 support via the pluggable HTTP/3 transport or the dynamic transport.

The high-level HTTP library supports cookies, authentication, redirection, connection pooling and a number of other features that are absent in the low-level HTTP/3 library.

The HTTP/3 client library has been designed for those applications that need low-level access to HTTP/3 features such as sessions, streams and frames, and this is quite a rare use case.

See also the correspondent HTTP/3 server library.

Introducing HTTP3Client

The Maven artifact coordinates for the HTTP/3 client library are the following:

<dependency>
  <groupId>org.eclipse.jetty.http3</groupId>
  <artifactId>http3-client</artifactId>
  <version>10.0.21-SNAPSHOT</version>
</dependency>

The main class is named org.eclipse.jetty.http3.client.HTTP3Client, and must be created, configured and started before use:

// Instantiate HTTP3Client.
HTTP3Client http3Client = new HTTP3Client();

// Configure HTTP3Client, for example:
http3Client.getHTTP3Configuration().setStreamIdleTimeout(15000);

// Start HTTP3Client.
http3Client.start();

When your application stops, or otherwise does not need HTTP3Client anymore, it should stop the HTTP3Client instance (or instances) that were started:

// Stop HTTP3Client.
http3Client.stop();

HTTP3Client allows client applications to connect to an HTTP/3 server. A session represents a single connection to an HTTP/3 server and is defined by class org.eclipse.jetty.http3.api.Session. A session typically has a long life — once the connection is established, it remains active until it is not used anymore (and therefore it is closed by the idle timeout mechanism), until a fatal error occurs (for example, a network failure), or if one of the peers decides unilaterally to close the connection.

HTTP/3 is a multiplexed protocol because it relies on the multiplexing capabilities of QUIC, the protocol based on UDP that transports HTTP/3 frames. Thanks to multiplexing, multiple HTTP/3 requests are sent on the same QUIC connection, or session. Each request/response cycle is represented by a stream. Therefore, a single session manages multiple concurrent streams. A stream has typically a very short life compared to the session: a stream only exists for the duration of the request/response cycle and then disappears.

Connecting to the Server

The first thing an application should do is to connect to the server and obtain a Session. The following example connects to the server:

// Address of the server's port.
SocketAddress serverAddress = new InetSocketAddress("localhost", 8444);

// Connect to the server, the CompletableFuture will be
// notified when the connection is succeeded (or failed).
CompletableFuture<Session.Client> sessionCF = http3Client.connect(serverAddress, new Session.Client.Listener() {});

// Block to obtain the Session.
// Alternatively you can use the CompletableFuture APIs to avoid blocking.
Session session = sessionCF.get();

Configuring the Session

The connect(…​) method takes a Session.Client.Listener parameter. This listener’s onPreface(…​) method is invoked just before establishing the connection to the server to gather the client configuration to send to the server. Client applications can override this method to change the default configuration:

SocketAddress serverAddress = new InetSocketAddress("localhost", 8444);
http3Client.connect(serverAddress, new Session.Client.Listener()
{
    @Override
    public Map<Long, Long> onPreface(Session session)
    {
        Map<Long, Long> configuration = new HashMap<>();

        // Add here configuration settings.

        return configuration;
    }
});

The Session.Client.Listener is notified of session events originated by the server such as receiving a SETTINGS frame from the server, or the server closing the connection, or the client timing out the connection due to idleness. Please refer to the Session.Client.Listener javadocs for the complete list of events.

Once a Session has been established, the communication with the server happens by exchanging frames.

Sending a Request

Sending an HTTP request to the server, and receiving a response, creates a stream that encapsulates the exchange of HTTP/3 frames that compose the request and the response.

In order to send an HTTP request to the server, the client must send a HEADERS frame. HEADERS frames carry the request method, the request URI and the request headers. Sending the HEADERS frame opens the Stream:

SocketAddress serverAddress = new InetSocketAddress("localhost", 8444);
CompletableFuture<Session.Client> sessionCF = http3Client.connect(serverAddress, new Session.Client.Listener() {});
Session.Client session = sessionCF.get();

// Configure the request headers.
HttpFields requestHeaders = HttpFields.build()
    .put(HttpHeader.USER_AGENT, "Jetty HTTP3Client 10.0.21-SNAPSHOT");

// The request metadata with method, URI and headers.
MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost:8444/path"), HttpVersion.HTTP_3, requestHeaders);

// The HTTP/3 HEADERS frame, with endStream=true
// to signal that this request has no content.
HeadersFrame headersFrame = new HeadersFrame(request, true);

// Open a Stream by sending the HEADERS frame.
session.newRequest(headersFrame, new Stream.Client.Listener() {});

Note how Session.newRequest(…​) takes a Stream.Client.Listener parameter. This listener is notified of stream events originated by the server such as receiving HEADERS or DATA frames that are part of the response, discussed in more details in the section below. Please refer to the Stream.Client.Listener javadocs for the complete list of events.

HTTP requests may have content, which is sent using the Stream APIs:

SocketAddress serverAddress = new InetSocketAddress("localhost", 8444);
CompletableFuture<Session.Client> sessionCF = http3Client.connect(serverAddress, new Session.Client.Listener() {});
Session.Client session = sessionCF.get();

// Configure the request headers.
HttpFields requestHeaders = HttpFields.build()
    .put(HttpHeader.CONTENT_TYPE, "application/json");

// The request metadata with method, URI and headers.
MetaData.Request request = new MetaData.Request("POST", HttpURI.from("http://localhost:8444/path"), HttpVersion.HTTP_3, requestHeaders);

// The HTTP/3 HEADERS frame, with endStream=false to
// signal that there will be more frames in this stream.
HeadersFrame headersFrame = new HeadersFrame(request, false);

// Open a Stream by sending the HEADERS frame.
CompletableFuture<Stream> streamCF = session.newRequest(headersFrame, new Stream.Client.Listener() {});

// Block to obtain the Stream.
// Alternatively you can use the CompletableFuture APIs to avoid blocking.
Stream stream = streamCF.get();

// The request content, in two chunks.
String content1 = "{\"greet\": \"hello world\"}";
ByteBuffer buffer1 = StandardCharsets.UTF_8.encode(content1);
String content2 = "{\"user\": \"jetty\"}";
ByteBuffer buffer2 = StandardCharsets.UTF_8.encode(content2);

// Send the first DATA frame on the stream, with endStream=false
// to signal that there are more frames in this stream.
CompletableFuture<Stream> dataCF1 = stream.data(new DataFrame(buffer1, false));

// Only when the first chunk has been sent we can send the second,
// with endStream=true to signal that there are no more frames.
dataCF1.thenCompose(s -> s.data(new DataFrame(buffer2, true)));
When sending two DATA frames consecutively, the second call to Stream.data(…​) must be done only when the first is completed, or a WritePendingException will be thrown. Use the CompletableFuture APIs to ensure that the second Stream.data(…​) call is performed when the first completed successfully.

Receiving a Response

Response events are delivered to the Stream.Client.Listener passed to Session.newRequest(…​).

An HTTP response is typically composed of a HEADERS frame containing the HTTP status code and the response headers, and optionally one or more DATA frames containing the response content bytes.

The HTTP/3 protocol also supports response trailers (that is, headers that are sent after the response content) that also are sent using a HEADERS frame.

A client application can therefore receive the HTTP/3 frames sent by the server by implementing the relevant methods in Stream.Client.Listener:

// Open a Stream by sending the HEADERS frame.
session.newRequest(headersFrame, new Stream.Client.Listener()
{
    @Override
    public void onResponse(Stream.Client stream, HeadersFrame frame)
    {
        MetaData metaData = frame.getMetaData();
        MetaData.Response response = (MetaData.Response)metaData;
        System.getLogger("http3").log(INFO, "Received response {0}", response);
    }

    @Override
    public void onDataAvailable(Stream.Client stream)
    {
        // Read a chunk of the content.
        Stream.Data data = stream.readData();
        if (data == null)
        {
            // No data available now, demand to be called back.
            stream.demand();
        }
        else
        {
            // Process the content.
            process(data.getByteBuffer());

            // Notify the implementation that the content has been consumed.
            data.complete();

            if (!data.isLast())
            {
                // Demand to be called back.
                stream.demand();
            }
        }
    }
});

Resetting a Request or Response

In HTTP/3, clients and servers have the ability to tell to the other peer that they are not interested anymore in either the request or the response, by resetting the stream.

The HTTP3Client APIs allow client applications to send and receive this "reset" event:

// Open a Stream by sending the HEADERS frame.
CompletableFuture<Stream> streamCF = session.newRequest(headersFrame, new Stream.Client.Listener()
{
    @Override
    public void onFailure(Stream.Client stream, long error, Throwable failure)
    {
        // The server reset this stream.
    }
});
Stream stream = streamCF.get();

// Reset this stream (for example, the user closed the application).
stream.reset(HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(), new ClosedChannelException());