HTTP/3 Server Library

In the vast majority of cases, server applications should use the generic, high-level, HTTP server library that also provides HTTP/3 support via the HTTP/3 connector and ConnectionFactorys as described in details here.

The low-level HTTP/3 server 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 client library.

Introduction

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

<dependency>
  <groupId>org.eclipse.jetty.http3</groupId>
  <artifactId>jetty-http3-server</artifactId>
  <version>12.0.15-SNAPSHOT</version>
</dependency>

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.

Server Setup

The low-level HTTP/3 support is provided by org.eclipse.jetty.http3.server.RawHTTP3ServerConnectionFactory and org.eclipse.jetty.http3.api.Session.Server.Listener:

// Create a Server instance.
Server server = new Server();

// HTTP/3 is always secure, so it always need a SslContextFactory.
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath("/path/to/keystore");
sslContextFactory.setKeyStorePassword("secret");

// The listener for session events.
Session.Server.Listener sessionListener = new Session.Server.Listener() {};

ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslContextFactory, Path.of("/path/to/pem/dir"));
// Configure the max number of requests per QUIC connection.
quicConfiguration.setMaxBidirectionalRemoteStreams(1024);

// Create and configure the RawHTTP3ServerConnectionFactory.
RawHTTP3ServerConnectionFactory http3 = new RawHTTP3ServerConnectionFactory(quicConfiguration, sessionListener);
http3.getHTTP3Configuration().setStreamIdleTimeout(15000);

// Create and configure the QuicServerConnector.
QuicServerConnector connector = new QuicServerConnector(server, quicConfiguration, http3);

// Add the Connector to the Server.
server.addConnector(connector);

// Start the Server so it starts accepting connections from clients.
server.start();

Where server applications using the high-level server library deal with HTTP requests and responses in Handlers, server applications using the low-level HTTP/3 server library deal directly with HTTP/3 sessions, streams and frames in a Session.Server.Listener implementation.

The Session.Server.Listener interface defines a number of methods that are invoked by the implementation upon the occurrence of HTTP/3 events, and that server applications can override to react to those events.

Please refer to the Session.Server.Listener javadocs for the complete list of events.

The first event is the accept event and happens when a client opens a new QUIC connection to the server and the server accepts the connection. This is the first occasion where server applications have access to the HTTP/3 Session object:

Session.Server.Listener sessionListener = new Session.Server.Listener()
{
    @Override
    public void onAccept(Session session)
    {
        SocketAddress remoteAddress = session.getRemoteSocketAddress();
        System.getLogger("http3").log(INFO, "Connection from {0}", remoteAddress);
    }
};

After the QUIC connection has been established, both client and server send an HTTP/3 SETTINGS frame to exchange their HTTP/3 configuration. This generates the preface event, where applications can customize the HTTP/3 settings by returning a map of settings that the implementation will send to the other peer:

Session.Server.Listener sessionListener = new Session.Server.Listener()
{
    @Override
    public Map<Long, Long> onPreface(Session session)
    {
        Map<Long, Long> settings = new HashMap<>();

        // Customize the settings

        return settings;
    }
};

Receiving a Request

Receiving an HTTP request from the client, and sending a response, creates a stream that encapsulates the exchange of HTTP/3 frames that compose the request and the response.

An HTTP request is made of a HEADERS frame, that carries the request method, the request URI and the request headers, and optional DATA frames that carry the request content.

Receiving the HEADERS frame opens the Stream:

Session.Server.Listener sessionListener = new Session.Server.Listener()
{
    @Override
    public Stream.Server.Listener onRequest(Stream.Server stream, HeadersFrame frame)
    {
        MetaData.Request request = (MetaData.Request)frame.getMetaData();

        // Return a Stream.Server.Listener to handle the request events,
        // for example request content events or a request reset.
        return new Stream.Server.Listener() {};
    }
};

Server applications should return a Stream.Server.Listener implementation from onRequest(…​) to be notified of events generated by the client, such as DATA frames carrying request content, or a reset event indicating that the client wants to reset the request, or an idle timeout event indicating that the client was supposed to send more frames but it did not.

The example below shows how to receive request content:

Session.Server.Listener sessionListener = new Session.Server.Listener()
{
    @Override
    public Stream.Server.Listener onRequest(Stream.Server stream, HeadersFrame frame)
    {
        MetaData.Request request = (MetaData.Request)frame.getMetaData();

        // Demand to be called back when data is available.
        stream.demand();

        // Return a Stream.Server.Listener to handle the request content.
        return new Stream.Server.Listener()
        {
            @Override
            public void onDataAvailable(Stream.Server stream)
            {
                // Read a chunk of the request content.
                Stream.Data data = stream.readData();

                if (data == null)
                {
                    // No data available now, demand to be called back.
                    stream.demand();
                }
                else
                {
                    // Get the content buffer.
                    ByteBuffer buffer = data.getByteBuffer();

                    // Consume the buffer, here - as an example - just log it.
                    System.getLogger("http3").log(INFO, "Consuming buffer {0}", buffer);

                    // Tell the implementation that the buffer has been consumed.
                    data.release();

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

Sending a Response

After receiving an HTTP request, a server application must send an HTTP response.

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 server application can send a response in this way:

Session.Server.Listener sessionListener = new Session.Server.Listener()
{
    @Override
    public Stream.Server.Listener onRequest(Stream.Server stream, HeadersFrame frame)
    {
        // Send a response after reading the request.
        MetaData.Request request = (MetaData.Request)frame.getMetaData();
        if (frame.isLast())
        {
            respond(stream, request);
            return null;
        }
        else
        {
            // Demand to be called back when data is available.
            stream.demand();
            return new Stream.Server.Listener()
            {
                @Override
                public void onDataAvailable(Stream.Server stream)
                {
                    Stream.Data data = stream.readData();
                    if (data == null)
                    {
                        stream.demand();
                    }
                    else
                    {
                        // Consume the request content.
                        data.release();

                        if (data.isLast())
                            respond(stream, request);
                        else
                            stream.demand();
                    }
                }
            };
        }
    }

    private void respond(Stream.Server stream, MetaData.Request request)
    {
        // Prepare the response HEADERS frame.

        // The response HTTP status and HTTP headers.
        MetaData.Response response = new MetaData.Response(HttpStatus.OK_200, null, HttpVersion.HTTP_3, HttpFields.EMPTY);

        if (HttpMethod.GET.is(request.getMethod()))
        {
            // The response content.
            ByteBuffer resourceBytes = getResourceBytes(request);

            // Send the HEADERS frame with the response status and headers,
            // and a DATA frame with the response content bytes.
            stream.respond(new HeadersFrame(response, false))
                .thenCompose(s -> s.data(new DataFrame(resourceBytes, true)));
        }
        else
        {
            // Send just the HEADERS frame with the response status and headers.
            stream.respond(new HeadersFrame(response, true));
        }
    }
};

Resetting a Request

A server application may decide that it does not want to accept the request. For example, it may throttle the client because it sent too many requests in a time window, or the request is invalid (and does not deserve a proper HTTP response), etc.

A request can be reset in this way:

Session.Server.Listener sessionListener = new Session.Server.Listener()
{
    @Override
    public Stream.Server.Listener onRequest(Stream.Server stream, HeadersFrame frame)
    {
        float requestRate = calculateRequestRate();

        if (requestRate > maxRequestRate)
        {
            stream.reset(HTTP3ErrorCode.REQUEST_REJECTED_ERROR.code(), new RejectedExecutionException());
            return null;
        }
        else
        {
            // The request is accepted.
            MetaData.Request request = (MetaData.Request)frame.getMetaData();
            // Return a Stream.Listener to handle the request events.
            return new Stream.Server.Listener() {};
        }
    }
};