WebTransport

WebTransport over HTTP/3 lets both peers open many independent streams inside a single QUIC connection, negotiated with one Extended CONNECT request.

Introduction

WebTransport is a transport protocol layered on top of HTTP/3 (RFC 9114), specified by draft-ietf-webtrans-http3. sgcWebSockets implements draft-09. A client opens a session with a single Extended CONNECT request carrying the :protocol = webtransport pseudo-header. Once the server answers :status = 200, both sides may open arbitrarily many bidirectional and unidirectional streams inside that session with no further round-trips. Because HTTP/3 runs over QUIC, those streams are independent and free from head-of-line blocking.

Think of it as the low-level sibling of WebSockets. Where a WebSocket gives you one ordered message channel, a WebTransport session gives you a whole connection worth of channels that you open and close as you please.

Extended CONNECT

WebTransport rides on Extended CONNECT, defined for HTTP/2 by RFC 8441 and ported to HTTP/3 by RFC 9220. A normal CONNECT request has no :protocol pseudo-header and no :path. An Extended CONNECT adds both, so a single request can bootstrap an arbitrary protocol on top of one request stream. For WebTransport the request looks like this:

:method    = CONNECT
:protocol  = webtransport
:scheme    = https
:authority = example.com
:path      = /echo

The request stream stays open for the lifetime of the session and is never used for HTTP body data. Its QUIC stream id becomes the session id.

The SETTINGS that enable it

A peer must advertise two HTTP/3 SETTINGS before WebTransport may be used:

SettingMeaning
SETTINGS_ENABLE_CONNECT_PROTOCOLRFC 9220. The peer accepts the :protocol pseudo-header, so Extended CONNECT is legal on this connection.
SETTINGS_WEBTRANSPORT_MAX_SESSIONSdraft-ietf-webtrans-http3 Section 3.1. The number of concurrent WebTransport sessions the peer will accept. A value of 0 means WebTransport is not offered.

On the TsgcHTTP3Server, setting H3Settings.Settings.WebTransportMaxSessions to a value greater than zero advertises both settings automatically and switches the connection into multi-stream mode. On the TsgcHTTP3Client, ServerSupportsWebTransport returns True once the peer SETTINGS frame has been read and it carried both values.

The object model

Two classes model a session, and both live in the sgcWebTransport unit:

Every WebTransport stream is a real QUIC stream prefixed with a small header that binds it to a session. A unidirectional stream starts with varint(0x54) + varint(SessionId), a bidirectional one with varint(0x41) + varint(SessionId). The library writes and parses that header for you: WriteData carries application bytes only, and OnData delivers application bytes only.

Server Flow

Enable WebTransport by setting H3Settings.Settings.WebTransportMaxSessions above zero, then handle OnWebTransportSession. The handler receives a live session and an accept flag, var aAccept in Delphi and C++Builder, e.Accept on .NET. It arrives as True on every stack, so doing nothing accepts the session; set it to False to reject, and the server replies :status = 404 and discards the session. When accepted, the server replies :status = 200, keeps the CONNECT stream open and registers the session.

Assign OnStream inside the handler, before returning, because peer-opened streams can arrive as soon as the session is registered.


// WebTransport is enabled only when WebTransportMaxSessions > 0
FServer := TsgcHTTP3Server.Create(nil);
FServer.H3Settings.Settings.WebTransportMaxSessions := 4;
FServer.OnWebTransportSession := OnWTSession;

procedure TfrmServer.OnWTSession(Sender: TObject;
  const aConnection: TsgcQUICServerConnection;
  const aSession: TsgcWebTransportSession; var aAccept: Boolean);
begin
  aAccept := True;   // already True on entry; set False to reject with 404
  aSession.OnStream := OnWTStream;
  aSession.OnClose := OnWTSessionClose;
end;

procedure TfrmServer.OnWTStream(Sender: TObject;
  const aStream: TsgcWebTransportStream);
begin
  aStream.OnData := OnWTStreamData;
end;

procedure TfrmServer.OnWTStreamData(Sender: TObject; const aData: TBytes);
begin
  // echo the bytes back on the same stream
  TsgcWebTransportStream(Sender).WriteData(aData);
end;

Client Flow

Call WebTransportConnect with the session URL. It opens a dedicated QUIC connection, sends the Extended CONNECT, waits for the response bounded by ConnectTimeout, and returns the session on :status = 200 or nil otherwise. It does not require a prior Connect call: if the client is already connected, it disconnects first, because a WebTransport session needs the CONNECT request to travel on the first client-initiated bidirectional stream.

The WebTransport connection is non-blocking, so nothing is read from the socket unless you pump it. Call WebTransportProcessEvents periodically from a timer or a background thread. Without it, OnData and OnStream never fire.


FClient := TsgcHTTP3Client.Create(nil);
FClient.TLSOptions.VerifyCertificate := False;
FClient.ConnectTimeout := 10000;

// Opens the QUIC connection and performs the Extended CONNECT. Returns nil if
// the server rejects the session or does not support WebTransport.
FSession := FClient.WebTransportConnect('https://127.0.0.1:4433/echo');
if Assigned(FSession) then
begin
  FStream := FSession.OpenStream(False);   // False = bidirectional
  FStream.OnData := OnStreamData;
  FStream.WriteData('Hello WebTransport');
  tmrPump.Enabled := True;
end;

procedure TfrmClient.tmrPumpTimer(Sender: TObject);
begin
  // Pump the QUIC connection so incoming stream data is harvested
  FClient.WebTransportProcessEvents;
end;

Datagrams: current status

WebTransport as specified also offers unreliable datagrams, and this library exposes the API for them: SendDatagram on the session and the OnDatagram event.

As shipped, WebTransport in sgcWebSockets is streams only. Datagrams do not reach the wire and do not arrive from it. This is true on the client, on the server, in Delphi, in C++Builder and on .NET. Streams work fully; datagrams do not work at all. The outbound call fails loudly rather than quietly, which is the one thing that changed. Concretely:

Why, and on which engine. sgcWebSockets has two QUIC engines, TsgcQUICMode = (qmCustom, qmBuiltin), chosen automatically when OpenSSL loads and not selectable by the application. qmCustom is the Pascal QUIC stack, used when the loaded library exposes the quictls QUIC TLS API. qmBuiltin is OpenSSL's own native QUIC, used otherwise.

WebTransport runs on qmBuiltin in every shipping configuration. The bundled OpenSSL 3.5 exports OSSL_QUIC_client_method but not the quictls QUIC TLS entry points, so qmBuiltin is what gets selected. It is also the only viable choice: qmCustom has no server support and cannot open the extra QUIC streams a WebTransport session needs.

And qmBuiltin is precisely the engine with no datagram path. WebTransport datagrams need the QUIC DATAGRAM extension (RFC 9221) underneath the HTTP Datagram layer (RFC 9297). OpenSSL's native QUIC does not implement RFC 9221, and its server side exposes no DATAGRAM write path, so there is nowhere to put the bytes. Writing them onto a reliable stream instead would corrupt the HTTP/3 framing, so the library refuses the call rather than break the connection. On .NET the cause is the same in a different guise: System.Net.Quic does not expose raw QUIC datagrams.

Yet the server still advertises SETTINGS_H3_DATAGRAM. Enabling WebTransport makes the server advertise it, together with SETTINGS_ENABLE_CONNECT_PROTOCOL and SETTINGS_WEBTRANSPORT_MAX_SESSIONS, even though no datagram can be carried. That is deliberate, not a stale setting. The WebTransport over HTTP/3 draft requires the setting, and browsers refuse to establish a session without it, so it is advertised for interoperability: without it the stream half of WebTransport, the half that works, would be unreachable from a browser.

What is there. The full encode and demultiplex path in sgcWebTransport is implemented and correct: Quarter-Stream-ID encode and decode, the session lookup by Quarter-Stream-ID, and the payload delivery. A datagram-capable QUIC backend can light datagrams up by supplying a real writer (assigning OnSendDatagramData, or overriding the virtual TsgcHTTP3Server.SendDatagram), setting DatagramsSupported := True on each session so the encoder runs instead of raising, and calling ProcessWebTransportDatagram on receipt. Until such a backend exists, treat datagram support as absent, and design your protocol on streams.

Requirements

Reference