TsgcWebTransportStream › Events › OnWriteBytes

OnWriteBytes Event

Transport hook. The stream's raw writer: every byte the stream sends passes through it on the way to the backing QUIC stream.

Syntax

property OnWriteBytes: TsgcWebTransportWriteEvent read FOnWriteBytes write FOnWriteBytes;

// event type
TsgcWebTransportWriteEvent = procedure(Sender: TObject;
  const aData: TBytes) of object;

// handler signature
procedure MyWriteBytes(Sender: TObject; const aData: TBytes);

Parameters

NameTypeDescription
SenderTObjectThe TsgcWebTransportStream doing the writing. Unlike the session-level writer, this identifies the stream, which is what lets the handler target the right QUIC stream. Cast it and read StreamId.
aDataconst TBytesThe exact bytes to put on the QUIC stream. Write them verbatim: no framing to add, nothing to interpret. This is either the WebTransport stream header, on the first call, or application payload from WriteData.

Default Value

Set by the session to a fallback writer that routes through the session-level OnWriteData. The transport replaces it with a per-stream writer from inside OnOpenStream, or just after AddIncomingStream for a peer-opened bidirectional stream.

Remarks

Do not assign this from application code. It is a transport hook, declared as an event property only because that is how a plain Pascal class takes a callback. It is already assigned on every stream you can reach, and overwriting it breaks the stream: your bytes go wherever your handler sends them, which is not the QUIC stream the peer is reading. Use WriteData to send data. This page exists for custom-transport authors.

Everything the stream sends goes through here, including the WebTransport stream header. OpenStream raises OnOpenStream so the transport can bind this hook, and only then writes varint(0x54|0x41) + varint(SessionId), which lands on the freshly bound writer. Application bytes follow on later calls. The handler cannot tell the two apart, and does not need to: write whatever arrives, unchanged.

This is the hook that makes streams independent. The session-level OnWriteData receives a byte array and the session, with no way to know which stream the bytes came from, so it can only write to one shared stream. OnWriteBytes receives the stream as Sender, so the handler can look up the matching QUIC stream and write there. That is what gives each WebTransport stream its own QUIC stream, and with it the freedom from head-of-line blocking that WebTransport exists to provide.

Which streams get a real writer. The HTTP/3 client and server bind a per-stream writer to streams opened locally through OpenStream, and to peer-opened bidirectional streams, so an echo from OnData replies on the same QUIC stream. Peer-opened unidirectional streams are receive-only and keep the fallback writer: writing to one is meaningless, and reports no error.

A robust handler resolves the QUIC stream by StreamId and falls back to the connection's default stream when it cannot, which is what the library's own writer does rather than raising.

This member is currently available in the Delphi/C++Builder implementation only. The .NET class has an equivalent internal WriteBytesHook property, not assignable from application code. There it is a plain Action<byte[]>, so the delegate closes over its QUIC stream instead of receiving the stream as Sender, and when it is null the managed WriteData silently discards the data.

Example


// What a custom QUIC transport assigns. This mirrors the library's own
// TsgcHTTP3Client_Base.DoWebTransportStreamWriteBytes. Applications call
// WriteData and never touch this.
procedure TMyTransport.DoOpenStream(Sender: TObject;
  const aStream: TsgcWebTransportStream);
var
  oQStream: TsgcSSLQUICStream;
begin
  oQStream := GetQUICComponent.OpenStream(FHost, FPort, aStream.Unidirectional);
  if Assigned(oQStream) then
  begin
    aStream.StreamId := oQStream.StreamId;
    aStream.OnWriteBytes := DoStreamWriteBytes;   // bind the per-stream writer
  end;
end;

procedure TMyTransport.DoStreamWriteBytes(Sender: TObject; const aData: TBytes);
var
  oWTStream: TsgcWebTransportStream;
  oQStream: TsgcSSLQUICStream;
begin
  // Sender identifies the stream, which is what makes this writer targeted
  if not (Sender is TsgcWebTransportStream) then
    Exit;
  oWTStream := TsgcWebTransportStream(Sender);

  oQStream := GetQUICComponent.FindStreamById(FHost, FPort, oWTStream.StreamId);
  if Assigned(oQStream) then
    // Write verbatim: aData is already exactly what belongs on the wire
    GetQUICComponent.WriteStreamData(FHost, FPort, oQStream, aData)
  else
    // No dedicated QUIC stream: degrade to the default stream rather than raise
    GetQUICComponent.Write(FHost, FPort, aData);
end;

Back to Events