TsgcWebTransportStream › Properties › StreamId
The QUIC stream id backing this stream. The transport sets it; applications read it.
property StreamId: Int64 read FStreamId write FStreamId;
Int64 (__int64 in C++Builder, long in C#).
-1 for a stream created by OpenStream, until the transport fills in the real id from inside OnOpenStream. A peer-opened stream is constructed with its real id and is never -1.
StreamId is the id QUIC assigned to the stream underneath this one. Its main use in application code is correlation: identifying which stream an OnData or OnClose callback came from, and matching a stream against a log or a packet capture.
It is read/write in Delphi and C++Builder, but do not write it. The setter exists so the transport can fill in the id from OnOpenStream, once the real QUIC stream is allocated. Assigning it yourself does not move the stream anywhere; it only corrupts the demultiplexer's view, so FindStreamById starts matching the wrong stream, or none. Treat it as read-only. It is genuinely read-only on .NET.
Reading the number. QUIC packs two bits of meaning into the low end of every stream id: bit 0 identifies the opener (0 = client, 1 = server), bit 1 the direction (0 = bidirectional, 1 = unidirectional). So 0, 4, 8... are client-opened bidirectional streams, 1, 5, 9... server-opened bidirectional, 2, 6, 10... client-opened unidirectional, and 3, 7, 11... server-opened unidirectional. Ids are unique within a QUIC connection, not globally, and are unrelated to ids in any other connection.
Do not confuse this with Session.SessionId. Both are QUIC stream ids in the same connection, but the session id names the Extended CONNECT stream, while this names one data stream inside the session.
A StreamId of -1 means no QUIC stream was ever bound. In the shipping HTTP/3 client and server that only happens when the QUIC stream could not be opened, leaving the stream on the session-level fallback writer. It is worth checking after OpenStream if you rely on the id.
.NET difference, worth knowing. On the managed stack, a stream you opened yourself gets a synthetic id: the session hands out a counter starting from SessionId, and the transport hook does not replace it with the real QUIC stream id. Peer-opened streams do carry the real QUIC id on both stacks. So on .NET, ids from a packet capture match peer-opened streams only, and a locally-opened stream's id is a local handle. The bytes on the wire are identical; only the value you can read back differs.
procedure TfrmClient.OnStreamData(Sender: TObject; const aData: TBytes);
var
oStream: TsgcWebTransportStream;
begin
// A single handler can serve many streams: StreamId says which one
oStream := TsgcWebTransportStream(Sender);
memoLog.Lines.Add(Format('stream %d: %s',
[oStream.StreamId, TEncoding.UTF8.GetString(aData)]));
end;
// Check the id after opening, when you depend on it
FStream := FSession.OpenStream(False);
if FStream.StreamId < 0 then
memoLog.Lines.Add('warning: no QUIC stream was bound to this stream')
else
memoLog.Lines.Add('bidi stream opened: id=' + IntToStr(FStream.StreamId));