Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,25 @@ public void h3() throws Exception
// end::h3[]
}

public void h2altsvc()
{
// tag::h2altsvc[]
HttpConfiguration httpConfig = new HttpConfiguration();
HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfig);

// Configure AltSvcCustomizer after connection factory creation.
httpConfig.getCustomizers().stream()
.filter(c -> c instanceof HTTP2ServerConnectionFactory.AltSvcCustomizer)
.map(HTTP2ServerConnectionFactory.AltSvcCustomizer.class::cast)
.findFirst()
.ifPresent(customizer ->
{
customizer.setMaxAge(Duration.ofHours(24));
customizer.setPersist(true);
});
// end::h2altsvc[]
}

public void conscrypt()
{
// tag::conscrypt[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,10 +533,22 @@ Alt-Svc: h3=":843"
----

The presence of this header indicates that protocol `h3` is available on the same host (since no host is defined before the port), but on port `843` (although it may be the same port `443`).
The `ma` (max-age) attribute specifies how long (in seconds) the client should cache this information (86400 seconds = 24 hours).
The HTTP/3 client may now initiate a QUIC connection on port `843` and make HTTP/3 requests.

NOTE: It is nowadays common to use the same port `443` for both HTTP/2 and HTTP/3. This does not cause problems because HTTP/2 listens on the TCP port `443`, while QUIC listens on the UDP port `443`.

TIP: The `Alt-Svc` header is automatically added to HTTP/2 responses by `HTTP2ServerConnectionFactory.AltSvcCustomizer`, advertising the HTTP/3 connector's port.
By default, only the port is advertised without additional attributes.
The `ma` (max-age) attribute can be configured via `AltSvcCustomizer.setMaxAge(Duration)` and the `persist` attribute via `AltSvcCustomizer.setPersist(boolean)`.

To configure the `AltSvcCustomizer`, retrieve it from the `HttpConfiguration` after creating the `HTTP2ServerConnectionFactory`:

[,java,indent=0,options=nowrap]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=h2altsvc]
----

It is therefore common for HTTP/3 clients to initiate connections using the HTTP/2 protocol over TCP, and if the server supports HTTP/3 switch to HTTP/3 as indicated by the server.

[plantuml]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@
package org.eclipse.jetty.http2.server;

import java.io.EOFException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.TimeoutException;

import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.HTTP2Cipher;
import org.eclipse.jetty.http2.HTTP2Stream;
Expand All @@ -35,6 +39,9 @@
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.NegotiatingServerConnection.CipherDiscriminator;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.StringUtil;
Expand All @@ -54,11 +61,13 @@ public HTTP2ServerConnectionFactory()
public HTTP2ServerConnectionFactory(@Name("config") HttpConfiguration httpConfiguration)
{
super(httpConfiguration);
httpConfiguration.addCustomizer(new AltSvcCustomizer());
}

public HTTP2ServerConnectionFactory(@Name("config") HttpConfiguration httpConfiguration, @Name("protocols") String... protocols)
{
super(httpConfiguration, protocols);
httpConfiguration.addCustomizer(new AltSvcCustomizer());
}

@Override
Expand Down Expand Up @@ -191,4 +200,83 @@ private void close(Stream stream, String reason)
stream.getSession().close(ErrorCode.PROTOCOL_ERROR.code, reason, Callback.NOOP);
}
}

/**
* <p>An {@link HttpConfiguration.Customizer} that adds the {@code Alt-Svc}
* header to HTTP/2 responses, advertising HTTP/3 support if an HTTP/3
* connector is available on the server.</p>
*/
public static class AltSvcCustomizer implements HttpConfiguration.Customizer
{
private Duration _maxAge;
private boolean _persist;

/**
* @return The max age for the Alt-Svc response header, or null if no max-age attribute should be sent.
*/
public Duration getMaxAge()
{
return _maxAge;
}

/**
* Sets the Alt-Svc max age.
*
* @param maxAge the max age for the Alt-Svc response header, or null if no max-age attribute should be sent.
*/
public void setMaxAge(Duration maxAge)
{
_maxAge = maxAge;
}

/**
* @return whether the persist parameter should be included in the Alt-Svc header.
*/
public boolean isPersist()
{
return _persist;
}

/**
* Sets whether to include the persist parameter in the Alt-Svc header.
* When true, adds {@code persist=1} to indicate the alternative service
* should be persisted across network changes.
*
* @param persist true to include the persist parameter, false otherwise.
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Alt-Svc#persist1">Alt-Svc persist parameter</a>
*/
public void setPersist(boolean persist)
{
_persist = persist;
}

@Override
public Request customize(Request request, HttpFields.Mutable responseHeaders)
{
if (HttpVersion.HTTP_2 != request.getConnectionMetaData().getHttpVersion())
return request;

Server server = request.getConnectionMetaData().getConnector().getServer();
for (Connector connector : server.getConnectors())
{
if (connector instanceof NetworkConnector nc &&
connector.getProtocols().contains("h3"))
{
int port = nc.getLocalPort();
if (port > 0)
{
StringBuilder altSvc = new StringBuilder();
altSvc.append(String.format("h3=\":%d\"", port));
if (_maxAge != null)
altSvc.append(String.format("; ma=%d", _maxAge.toSeconds()));
if (_persist)
altSvc.append("; persist=1");
responseHeaders.add(HttpHeader.ALT_SVC, altSvc.toString());
}
break;
}
}
return request;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;

import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http3.HTTP3Stream;
import org.eclipse.jetty.http3.api.Session;
import org.eclipse.jetty.http3.api.Stream;
Expand All @@ -28,11 +25,7 @@
import org.eclipse.jetty.http3.server.internal.HTTP3StreamServer;
import org.eclipse.jetty.http3.server.internal.HttpStreamOverHTTP3;
import org.eclipse.jetty.http3.server.internal.ServerHTTP3StreamConnection;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.thread.Invocable;
import org.eclipse.jetty.util.thread.ThreadPool;
Expand All @@ -49,20 +42,6 @@ public HTTP3ServerConnectionFactory()
public HTTP3ServerConnectionFactory(HttpConfiguration configuration)
{
super(configuration, new HTTP3SessionListener());
configuration.addCustomizer(new AltSvcCustomizer());
}

private static class AltSvcCustomizer implements HttpConfiguration.Customizer
{
@Override
public Request customize(Request request, HttpFields.Mutable responseHeaders)
{
ConnectionMetaData connectionMetaData = request.getConnectionMetaData();
Connector connector = connectionMetaData.getConnector();
if (connector instanceof NetworkConnector networkConnector && HttpVersion.HTTP_2 == connectionMetaData.getHttpVersion())
responseHeaders.add(HttpHeader.ALT_SVC, String.format("h3=\":%d\"", networkConnector.getLocalPort()));
return request;
}
}

private static class HTTP3SessionListener implements HTTP3SessionServer.Listener
Expand Down
Loading