From e1bdf92321f044d7b37193a7abd219c4129a02b9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 23:04:40 +0200 Subject: [PATCH] C#: an unsendable goodbye names the failure by a fixed code EndUnsendable put the SocketException's message on the wire, the platform's wording. SendErrorCode names a permanent failure EMSGSIZE, EINVAL or EAFNOSUPPORT, anything else UNKNOWN, so every runtime's unsendable goodbye reads "packet could not be sent: CODE" with the same codes. Co-Authored-By: Claude Opus 5.5 --- src/GameCult.Networking/CultNetTransport.cs | 19 +++++++++++++++++-- .../CultNetRudpSendFailureTests.cs | 12 ++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/GameCult.Networking/CultNetTransport.cs b/src/GameCult.Networking/CultNetTransport.cs index a6102de3..ac2c2952 100644 --- a/src/GameCult.Networking/CultNetTransport.cs +++ b/src/GameCult.Networking/CultNetTransport.cs @@ -1490,15 +1490,30 @@ namespace GameCult.Networking internal const string UnsendablePacketReason = "packet could not be sent"; + /// + /// The fixed name of a failed send's cause: EMSGSIZE, EINVAL or EAFNOSUPPORT for a failure + /// that can never pass (), UNKNOWN for anything else, as + /// every runtime's goodbye names it. Never the error's own message, which is the platform's + /// wording. + /// + internal static string SendErrorCode(SocketException error) => error.SocketErrorCode switch + { + SocketError.MessageSize => "EMSGSIZE", + SocketError.InvalidArgument => "EINVAL", + SocketError.AddressFamilyNotSupported => "EAFNOSUPPORT", + _ => "UNKNOWN", + }; + /// /// Ends a session that owes its peer a packet that can never be sent as built. Resending it /// would fail forever, and dropping it would leave a reliable sequence the peer waits on for - /// good, so the session cannot be kept. The goodbye's reason names the error. + /// good, so the session cannot be kept. The goodbye's reason names the failure by its fixed + /// name (). /// internal CultNetRudpPacket EndUnsendable(SocketException error) { ResetPeerState(); - return CreateDisconnect(Encoding.UTF8.GetBytes(UnsendablePacketReason + ": " + error.Message)); + return CreateDisconnect(Encoding.UTF8.GetBytes(UnsendablePacketReason + ": " + SendErrorCode(error))); } internal uint NextSequence => _nextSequence; diff --git a/tests/GameCult.Networking.Tests/CultNetRudpSendFailureTests.cs b/tests/GameCult.Networking.Tests/CultNetRudpSendFailureTests.cs index 81d255f7..3decfa46 100644 --- a/tests/GameCult.Networking.Tests/CultNetRudpSendFailureTests.cs +++ b/tests/GameCult.Networking.Tests/CultNetRudpSendFailureTests.cs @@ -137,6 +137,18 @@ namespace GameCult.Networking.Tests // caller-directed send throws it and queues nothing; inside a poll it ends that peer's // session and never the poll. + [Test] + public void An_unsendable_goodbye_names_the_failure_by_a_fixed_code_never_its_message() + { + var session = new CultNetRudpSession(new CultNetRudpSessionOptions { ConnectionId = 1 }); + Assert.That( + Encoding.UTF8.GetString(session.EndUnsendable(new SocketException((int)SocketError.MessageSize)).Payload!), + Is.EqualTo("packet could not be sent: EMSGSIZE")); + Assert.That(CultNetRudpSession.SendErrorCode(new SocketException((int)SocketError.InvalidArgument)), Is.EqualTo("EINVAL")); + Assert.That(CultNetRudpSession.SendErrorCode(new SocketException((int)SocketError.AddressFamilyNotSupported)), Is.EqualTo("EAFNOSUPPORT")); + Assert.That(CultNetRudpSession.SendErrorCode(new SocketException((int)SocketError.ConnectionRefused)), Is.EqualTo("UNKNOWN")); + } + private static bool IsUnsendableReason(byte[]? reason) { var text = Encoding.UTF8.GetString(reason ?? Array.Empty());