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 ConnectionFactory
s 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.
Support for the QUIC protocol underlying HTTP/3 is provided by default via Jetty classes that wrap the Quiche native library.
See also the correspondent HTTP/3 client library.
Introduction
The Maven artifact coordinates for the HTTP/3 server library are the following:
<dependency>
<groupId>org.eclipse.jetty.http3</groupId>
<artifactId>jetty-http3-server</artifactId>
<version>12.1.0-SNAPSHOT</version>
</dependency>
The HTTP/3 protocol is layered on top of the underlying QUIC protocol.
When a client wants to establish a connection with a server, the QUIC protocol establishes the connection with the server, represented by a QUIC session.
A QUIC 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.
There is a 1-to-1 relationship between a QUIC session and an HTTP/3 session, defined by class org.eclipse.jetty.http3.api.Session
.
The QUIC protocol is multiplexed, that is it allows multiple, independent, QUIC streams to exchange data to and from the server, over a single connection. Therefore, a single QUIC session manages multiple concurrent QUIC streams.
There is a 1-to-1 relationship between a QUIC stream and an HTTP/3 stream, defined by class org.eclipse.jetty.http3.api.Stream
.
Each HTTP/3 request/response cycle is carried by an HTTP/3 stream, and 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() {};
QuicheServerQuicConfiguration serverQuicConfig = new QuicheServerQuicConfiguration(Path.of("/path/to/pem/dir"));
// Configure the max number of requests per QUIC connection.
serverQuicConfig.setBidirectionalMaxStreams(1024 * 1024);
// Create and configure the RawHTTP3ServerConnectionFactory.
RawHTTP3ServerConnectionFactory http3 = new RawHTTP3ServerConnectionFactory(sessionListener);
http3.getHTTP3Configuration().setStreamIdleTimeout(15000);
// Create and configure the QuicheServerConnector.
QuicheServerConnector connector = new QuicheServerConnector(server, sslContextFactory, serverQuicConfig, 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 Handler
s, 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.Server 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 must send an HTTP/3 SETTINGS
frame to exchange their HTTP/3 configuration.
This generates the preface event, where applications can customize the HTTP/3 configuration 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, for example:
settings.put(SettingsFrame.MAX_BLOCKED_STREAMS, 16L);
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(Session.Server session, HeadersFrame frame)
{
// Return a Stream.Server.Listener to handle the request events,
// for example request content events or request cancellation.
return new Stream.Server.Listener()
{
@Override
public void onRequest(Stream.Server stream, HeadersFrame frame)
{
// Process the request method, URI and headers.
MetaData.Request request = (MetaData.Request)frame.getMetaData();
}
};
}
};
Server applications should return a Stream.Server.Listener
implementation from onRequest(...)
to be notified of events generated by the client, such as HEADERS
frame carrying the request headers, DATA
frames carrying request content, or a failure event indicating that the client wants to cancel 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(Session.Server session, HeadersFrame frame)
{
// Return a Stream.Server.Listener to handle the request.
return new Stream.Server.Listener()
{
@Override
public void onRequest(Stream.Server stream, HeadersFrame frame)
{
// Process the request method, URI and headers.
MetaData.Request request = (MetaData.Request)frame.getMetaData();
// Demand to be called back when data is available.
stream.demand();
}
@Override
public void onDataAvailable(Stream.Server stream)
{
// Read a chunk of the request content.
Content.Chunk chunk = stream.read();
if (chunk == null)
{
// No data available now, demand to be called back.
stream.demand();
}
else
{
// Get the content buffer.
ByteBuffer buffer = chunk.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.
chunk.release();
if (!chunk.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(Session.Server session, HeadersFrame frame)
{
MetaData.Request request = (MetaData.Request)frame.getMetaData();
return new Stream.Server.Listener()
{
@Override
public void onRequest(Stream.Server stream, HeadersFrame frame)
{
// Send a response after reading the request.
if (frame.isLast())
{
// No request content, respond immediately.
respond(stream, request);
}
else
{
// Demand to be called back when request content is available.
stream.demand();
}
}
@Override
public void onDataAvailable(Stream.Server stream)
{
Content.Chunk chunk = stream.read();
if (chunk == null)
{
stream.demand();
}
else
{
// Consume the request content.
chunk.release();
if (chunk.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), new Promise.Invocable.NonBlocking<>()
{
@Override
public void succeeded(Stream result)
{
result.data(new DataFrame(resourceBytes, true), Promise.Invocable.noop());
}
});
}
else
{
// Send just the HEADERS frame with the response status and headers.
stream.respond(new HeadersFrame(response, true), Promise.Invocable.noop());
}
}
};
Terminating 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 abruptly terminated in this way:
Session.Server.Listener sessionListener = new Session.Server.Listener()
{
@Override
public Stream.Server.Listener onRequest(Session.Server session, HeadersFrame frame)
{
// Return a Stream.Server.Listener to handle the request events.
return new Stream.Server.Listener()
{
@Override
public void onRequest(Stream.Server stream, HeadersFrame frame)
{
float requestRate = calculateRequestRate();
if (requestRate > maxRequestRate)
{
// The request is rejected.
stream.disconnect(HTTP3ErrorCode.REQUEST_REJECTED_ERROR.code(), new RejectedExecutionException(), Promise.Invocable.noop());
}
else
{
// The request is accepted.
}
}
};
}
};