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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 23:04:40 +02:00
commit e1bdf92321
2 changed files with 29 additions and 2 deletions

View file

@ -1490,15 +1490,30 @@ namespace GameCult.Networking
internal const string UnsendablePacketReason = "packet could not be sent";
/// <summary>
/// The fixed name of a failed send's cause: EMSGSIZE, EINVAL or EAFNOSUPPORT for a failure
/// that can never pass (<see cref="IsPermanentSendError"/>), UNKNOWN for anything else, as
/// every runtime's goodbye names it. Never the error's own message, which is the platform's
/// wording.
/// </summary>
internal static string SendErrorCode(SocketException error) => error.SocketErrorCode switch
{
SocketError.MessageSize => "EMSGSIZE",
SocketError.InvalidArgument => "EINVAL",
SocketError.AddressFamilyNotSupported => "EAFNOSUPPORT",
_ => "UNKNOWN",
};
/// <summary>
/// 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 (<see cref="SendErrorCode"/>).
/// </summary>
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;

View file

@ -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<byte>());