Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/clients/ioxide.file/ioxide.file.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.file</RootNamespace>

<PackageId>ioxide.file</PackageId>
<Version>0.5.194</Version>
<Version>0.6.208</Version>
<Authors>MDA2AV</Authors>
<Description>File serving for the ioxide io_uring runtime: immutable asset snapshots with baked responses, pooled positional ring reads, atomic reloads.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.httpclient/ioxide.httpclient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.httpclient</RootNamespace>

<PackageId>ioxide.httpclient</PackageId>
<Version>0.5.194</Version>
<Version>0.6.208</Version>
<Authors>MDA2AV</Authors>
<Description>The ring-native HTTP/1.1 client for the ioxide io_uring runtime - the upstream leg between a proxy and an origin. Connections are opened on the reactor thread that will use them, so a request never crosses a thread on its way out or back, and every response resumes the awaiting handler inline on its own reactor. Includes client-side TLS (SNI, ALPN, certificate verification and client certificates for mutual TLS) for https:// origins. Depends on ioxide core alone: no protocol package, no native asset.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.pg/ioxide.pg.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.pg</RootNamespace>

<PackageId>ioxide.pg</PackageId>
<Version>0.5.194</Version>
<Version>0.6.208</Version>
<Authors>MDA2AV</Authors>
<Description>Postgres driver for the ioxide io_uring runtime: pooled ring-native connections per reactor, ring-native connect and handshake, inline completion resume.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.redis/ioxide.redis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.redis</RootNamespace>

<PackageId>ioxide.redis</PackageId>
<Version>0.5.194</Version>
<Version>0.6.208</Version>
<Authors>MDA2AV</Authors>
<Description>Redis client for the ioxide io_uring runtime: pooled ring-native connections per reactor, full RESP2 protocol, a generic command API plus typed helpers (strings, keys, hashes, lists, sets, sorted sets, pub/sub, transactions, scripting), and pipelining. Inline completion resume.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/ioxide/ioxide.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide</RootNamespace>

<PackageId>ioxide</PackageId>
<Version>0.5.194</Version>
<Version>0.6.208</Version>
<Authors>MDA2AV</Authors>
<Description>A shared-nothing io_uring runtime for .NET: one ring per reactor thread, inline completions, zero native dependencies. The engine - reactor, connection, and the IRingHost client seam. Includes TLS termination: the OpenSSL handshake driven over the ring, then kernel TLS (kTLS) transmit offload, so handlers keep writing plaintext. TLS needs OpenSSL 3 and the Linux tls module; nothing else does, and neither is loaded unless you use it.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.http2/ioxide.http2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.http2</RootNamespace>

<PackageId>ioxide.http2</PackageId>
<Version>0.5.194</Version>
<Version>0.6.208</Version>
<Authors>MDA2AV</Authors>
<Description>Pure-C# HTTP/2 for the ioxide io_uring runtime: framing, HPACK (static and dynamic tables, Huffman) and flow control, with zero native code. Serves h2c with prior knowledge and h2 over TLS by ALPN, buffered or streamed in either direction.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
8 changes: 6 additions & 2 deletions src/protocols/ioxide.http3/Http3Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public sealed partial class Http3Connection
// RFC 9114 section 8.1. The code a protocol error closes the connection with; the peer is
// entitled to know WHY it was dropped, and until now it was told nothing at all.
private const ulong H3GeneralProtocolError = 0x0101;
// RFC 9114 8.1 draws the line by WHY the frame is wrong, and the two are not interchangeable:
// UNEXPECTED is a frame that is not permitted in this state or on this stream, ERROR is one
// whose layout or size is invalid. A peer debugging its own framing is told different things.
private const ulong H3FrameUnexpected = 0x0105;
private const ulong H3FrameError = 0x0106;
private const ulong H3ExcessiveLoad = 0x0107;
private const ulong QpackDecompressionFailed = 0x0200;
Expand Down Expand Up @@ -301,7 +305,7 @@ private void FeedRequest(long sid, ReqStream rs, ReadOnlySpan<byte> data, bool f
{
if (!rs.HeadersDone)
{
Fatal("DATA before HEADERS", H3FrameError);
Fatal("DATA before HEADERS", H3FrameUnexpected);
return;
}
rs.State = len == 0 ? ParseState.FrameHeader : ParseState.DataPayload;
Expand All @@ -326,7 +330,7 @@ private void FeedRequest(long sid, ReqStream rs, ReadOnlySpan<byte> data, bool f
}
else if (type is 0x3 or 0x4 or 0x5 or 0x7 or 0xD)
{
Fatal($"frame 0x{type:x} unexpected on a request stream", H3FrameError);
Fatal($"frame 0x{type:x} unexpected on a request stream", H3FrameUnexpected);
return;
}
else
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.http3/ioxide.http3.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.http3</RootNamespace>

<PackageId>ioxide.http3</PackageId>
<Version>0.5.194</Version>
<Version>0.6.208</Version>
<Authors>MDA2AV</Authors>
<Description>Pure C# HTTP/3 for the ioxide io_uring runtime: frame parsing, QPACK (static table + Huffman) and request dispatch with zero native dependencies. Rides any QuicConnection via its stream read surface - engine-agnostic, drop-in alternative to ioxide.nghttp3.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.nghttp2/ioxide.nghttp2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.nghttp2</RootNamespace>

<PackageId>ioxide.nghttp2</PackageId>
<Version>0.5.194</Version>
<Version>0.6.208</Version>
<Authors>MDA2AV</Authors>
<Description>HTTP/2 for the ioxide io_uring runtime: framing, HPACK and flow control from nghttp2, statically linked behind a small shim with no external dependencies beyond libc. Serves HTTP/2 over any TcpConnection - h2c with prior knowledge, or h2 over TLS via ALPN. nghttp2 is sans-I/O, so ioxide keeps the ring and the loop.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private static unsafe void Fault(void* user, Exception e)
{
Nghttp3Connection connection = From(user);
connection._callbackFault ??= e;
connection._protocolFailed = true;
connection.FailInternal();
Console.Error.WriteLine($"[ioxide.nghttp3] callback faulted, failing the connection: {e}");
}
catch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private unsafe void QueueResponse(long streamId, Nghttp3Response response)
if (submitResult != 0)
{
Console.Error.WriteLine($"[ioxide.nghttp3] submit_response failed: {Nghttp3.StrError(submitResult)}");
_protocolFailed = true;
FailProtocol(submitResult);
}
}

Expand Down Expand Up @@ -56,7 +56,7 @@ private unsafe bool PumpEgress()
if (producedBytes < 0)
{
Console.Error.WriteLine($"[ioxide.nghttp3] writev failed: {Nghttp3.StrError((int)producedBytes)}");
_protocolFailed = true;
FailProtocol((int)producedBytes);
return producedAnything;
}
if (producedBytes > 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private unsafe void PushToEngine(in QuicRecvRing.Delivery item)
if (eventResult < 0)
{
Console.Error.WriteLine($"[ioxide.nghttp3] stream {item.Kind} handling failed: {Nghttp3.StrError(eventResult)}");
_protocolFailed = true;
FailProtocol(eventResult);
}

return;
Expand All @@ -62,7 +62,7 @@ private unsafe void PushToEngine(in QuicRecvRing.Delivery item)
if (readResult < 0)
{
Console.Error.WriteLine($"[ioxide.nghttp3] read_stream failed: {Nghttp3.StrError((int)readResult)}");
_protocolFailed = true;
FailProtocol((int)readResult);

return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public async Task RunBufferedAsync(Func<Nghttp3Request, Nghttp3Response> handler

if (_nghttp3Handle == 0 && !_protocolFailed && !TrySetup())
{
_protocolFailed = true;
FailInternal();
}

while (_quicConnection.TryGetDelivery(in snapshot, out QuicRecvRing.Delivery item))
Expand Down Expand Up @@ -46,6 +46,11 @@ public async Task RunBufferedAsync(Func<Nghttp3Request, Nghttp3Response> handler
}
finally
{
// Before letting go, because letting go is all the transport sees: DecRef neither
// closes nor unregisters, so a connection dropped without a code stays routable until
// the idle sweep while the client waits on a request that will never be answered.
CloseWithPeerCode();

_quicConnection.DecRef();
Dispose();
}
Expand All @@ -61,7 +66,7 @@ public async Task RunBufferedAsync(Func<Nghttp3Request, ValueTask<Nghttp3Respons

if (_nghttp3Handle == 0 && !_protocolFailed && !TrySetup())
{
_protocolFailed = true;
FailInternal();
}

while (_quicConnection.TryGetDelivery(in snapshot, out QuicRecvRing.Delivery item))
Expand Down Expand Up @@ -89,6 +94,11 @@ public async Task RunBufferedAsync(Func<Nghttp3Request, ValueTask<Nghttp3Respons
}
finally
{
// Before letting go, because letting go is all the transport sees: DecRef neither
// closes nor unregisters, so a connection dropped without a code stays routable until
// the idle sweep while the client waits on a request that will never be answered.
CloseWithPeerCode();

_quicConnection.DecRef();
Dispose();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public async Task RunStreamingAsync(Func<Nghttp3Request, ValueTask<Nghttp3Respon

if (_nghttp3Handle == 0 && !_protocolFailed && !TrySetup())
{
_protocolFailed = true;
FailInternal();
}

while (_quicConnection.TryGetDelivery(in snapshot, out QuicRecvRing.Delivery item))
Expand Down Expand Up @@ -60,6 +60,11 @@ public async Task RunStreamingAsync(Func<Nghttp3Request, ValueTask<Nghttp3Respon
FireBodyWakes();
_sinks.Clear();

// Before letting go, because letting go is all the transport sees: DecRef neither
// closes nor unregisters, so a connection dropped without a code stays routable until
// the idle sweep while the client waits on a request that will never be answered.
CloseWithPeerCode();

_quicConnection.DecRef();
Dispose();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public async Task RunStreamedResponseAsync(Func<Nghttp3Request, Nghttp3ResponseW

if (_nghttp3Handle == 0 && !_protocolFailed && !TrySetup())
{
_protocolFailed = true;
FailInternal();
}

while (_quicConnection.TryGetDelivery(in snapshot, out QuicRecvRing.Delivery item))
Expand Down Expand Up @@ -97,6 +97,9 @@ public async Task RunStreamedResponseAsync(Func<Nghttp3Request, Nghttp3ResponseW
_sinks.Clear();

// Unpark anything still waiting on a pass; IsBroken makes them return rather than loop.
// Deliberately the bare flag and NOT FailInternal(): this runs on a response that
// completed perfectly well, and giving it a peer code would close every successful
// streamed connection with an error.
_protocolFailed = true;
ReleasePassWaiters();

Expand All @@ -111,6 +114,11 @@ public async Task RunStreamedResponseAsync(Func<Nghttp3Request, Nghttp3ResponseW
pooled.Release(); // the native blocks die with the connection, not the stream
}

// Before letting go, because letting go is all the transport sees: DecRef neither
// closes nor unregisters, so a connection dropped without a code stays routable until
// the idle sweep while the client waits on a request that will never be answered.
CloseWithPeerCode();

_quicConnection.DecRef();
Dispose();
}
Expand Down Expand Up @@ -283,7 +291,7 @@ internal unsafe void SubmitStreamedHeaders(long streamId, Nghttp3Response respon
if (result != 0)
{
Console.Error.WriteLine($"[ioxide.nghttp3] submit_response_stream failed: {Nghttp3.StrError(result)}");
_protocolFailed = true;
FailProtocol(result);
}
}

Expand Down
52 changes: 51 additions & 1 deletion src/protocols/ioxide.nghttp3/Connection/Nghttp3Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,56 @@ public sealed partial class Nghttp3Connection : IDisposable
// RFC 9114 H3_NO_ERROR - the application error code a graceful close carries.
private const ulong H3NoError = 0x100;

// RFC 9114 H3_INTERNAL_ERROR: what a failure that cannot name itself closes with.
private const ulong H3InternalError = 0x102;

// What the peer must be told, or null when there is nothing to tell it. Deliberately NOT
// _protocolFailed: that flag also means "stop looping", and one site sets it on a perfectly
// clean streamed response purely to unpark waiters - closing on it would send an error code to
// every client that received a streamed body successfully.
//
// A connection that dies without a code leaves the client waiting on a request that will never
// be answered, and no idle bound rescues it: the shim sets no max_idle_timeout, and the
// transport's 60 s sweep is refreshed by ANY inbound datagram, including ones ngtcp2 discards.
private ulong? _peerCode;

/// <summary>
/// A failure nghttp3 reported, and the h3 code it means. The mapping is nghttp3's own
/// (<c>nghttp3_err_infer_quic_app_error_code</c>) rather than a table kept here: it owns which
/// of its errors are H3_FRAME_ERROR, H3_MESSAGE_ERROR or QPACK_DECOMPRESSION_FAILED, and
/// anything it does not recognise becomes H3_INTERNAL_ERROR.
/// </summary>
private void FailProtocol(int libError)
{
// First failure wins - later ones are usually consequences of it, and the peer can only be
// told once.
_peerCode ??= Nghttp3.ih3_app_error_code(libError);
_protocolFailed = true;
}

/// <summary>
/// A failure of ours rather than of the protocol - a control stream that would not open, a
/// callback that threw. H3_INTERNAL_ERROR is the honest code for it.
/// </summary>
private void FailInternal()
{
_peerCode ??= H3InternalError;
_protocolFailed = true;
}

/// <summary>
/// Tell the peer why, if there is a why. Called from every run loop's finally, where the
/// connection is being let go: without it the transport keeps the connection registered and
/// routable, and the client's request never completes.
/// </summary>
private void CloseWithPeerCode()
{
if (_peerCode is ulong code)
{
_quicConnection.Close(code);
}
}

private readonly Dictionary<long, Nghttp3Request> _requests = new();
private readonly List<long> _readyStreamIds = [];
private readonly byte[] _egress = new byte[16 * 1024];
Expand Down Expand Up @@ -101,7 +151,7 @@ public void Shutdown()
if (shutdownResult != 0)
{
Console.Error.WriteLine($"[ioxide.nghttp3] shutdown failed: {Nghttp3.StrError(shutdownResult)}");
_protocolFailed = true;
FailProtocol(shutdownResult);
return;
}
PumpEgress(); // the GOAWAY leaves now, ahead of any in-flight responses
Expand Down
5 changes: 5 additions & 0 deletions src/protocols/ioxide.nghttp3/Interop/Nghttp3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ internal struct Callbacks
/// block). Unknown/already-closed ids (e.g. uni streams) are tolerated and return 0.</summary>
[DllImport(Lib)] internal static extern int ih3_close_stream(nint connection, long streamId, ulong appError);

/// <summary>The h3 application error code a library error means. nghttp3 owns this mapping -
/// which of its errors are H3_FRAME_ERROR, H3_MESSAGE_ERROR, QPACK_DECOMPRESSION_FAILED - so a
/// connection that must tell its peer why it died asks rather than keeping a table.</summary>
[DllImport(Lib)] internal static extern ulong ih3_app_error_code(int libError);

// --- client side (ioxide.httpclient) ------------------------------------------------------

/// <summary>Create the client-side nghttp3 connection; same event surface as the server one,
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.nghttp3/ioxide.nghttp3.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.nghttp3</RootNamespace>

<PackageId>ioxide.nghttp3</PackageId>
<Version>0.5.194</Version>
<Version>0.6.208</Version>
<Authors>MDA2AV</Authors>
<Description>HTTP/3 layer for the ioxide io_uring runtime: nghttp3 (H3 + QPACK) bundled as a single self-contained native library with no external dependencies. Rides any QuicConnection via its stream read surface - engine-agnostic, no ioxide.ngtcp2 dependency.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
10 changes: 10 additions & 0 deletions src/protocols/ioxide.nghttp3/native/ioxide_nghttp3_shim.c
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,16 @@ int ih3_shutdown_stream_write(ih3_conn *c, int64_t stream_id)
return 0;
}

/* The h3 application error code a library error means, straight from nghttp3 rather than from a
* table of our own: it owns the NGHTTP3_ERR_* -> RFC 9114 mapping and knows which of its errors are
* H3_FRAME_ERROR, H3_MESSAGE_ERROR, QPACK_DECOMPRESSION_FAILED and so on. Anything it does not
* recognise becomes H3_INTERNAL_ERROR, which is the right answer for "we failed and cannot say
* why". */
uint64_t ih3_app_error_code(int liberr)
{
return nghttp3_err_infer_quic_app_error_code(liberr);
}

int ih3_close_stream(ih3_conn *c, int64_t stream_id, uint64_t app_error)
{
int rv = nghttp3_conn_close_stream(c->conn, stream_id, app_error);
Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion src/protocols/ioxide.ngtcp2/ioxide.ngtcp2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.ngtcp2</RootNamespace>

<PackageId>ioxide.ngtcp2</PackageId>
<Version>0.5.194</Version>
<Version>0.6.208</Version>
<Authors>MDA2AV</Authors>
<Description>QUIC engine for the ioxide io_uring runtime: ngtcp2 + picotls bundled as a single self-contained native library (only system dependency: libcrypto.so.3 / OpenSSL 3.x). Plugs into the reactor's QUIC transport via QuicConnection. Server side; engine bindings in progress.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/serving/ioxide.Kestrel/ioxide.Kestrel.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.Kestrel</RootNamespace>

<PackageId>ioxide.Kestrel</PackageId>
<Version>0.5.194</Version>
<Version>0.6.208</Version>
<Authors>MDA2AV</Authors>
<Description>ASP.NET Core Kestrel transport backed by the ioxide io_uring runtime: one reactor (ring) per core, SO_REUSEPORT load-balanced, with Kestrel's HTTP request loop pinned to the reactor thread. Drop-in via UseIoxide().</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
Loading
Loading