From 8bbfa54058094701a61df9f0913d75e6c0a47297 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 22 Feb 2026 23:33:13 -0500 Subject: [PATCH] feat: add ClientClosedReason enum with 16 close reason values --- src/NATS.Server/ClientClosedReason.cs | 51 +++++++++++++++++++ .../ClientClosedReasonTests.cs | 24 +++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/NATS.Server/ClientClosedReason.cs create mode 100644 tests/NATS.Server.Tests/ClientClosedReasonTests.cs diff --git a/src/NATS.Server/ClientClosedReason.cs b/src/NATS.Server/ClientClosedReason.cs new file mode 100644 index 0000000..01eec58 --- /dev/null +++ b/src/NATS.Server/ClientClosedReason.cs @@ -0,0 +1,51 @@ +namespace NATS.Server; + +/// +/// Reason a client connection was closed. +/// Corresponds to Go server/client.go ClosedState (subset for single-server scope). +/// +public enum ClientClosedReason +{ + None = 0, + ClientClosed, + AuthenticationTimeout, + AuthenticationViolation, + TlsHandshakeError, + SlowConsumerPendingBytes, + SlowConsumerWriteDeadline, + WriteError, + ReadError, + ParseError, + StaleConnection, + ProtocolViolation, + MaxPayloadExceeded, + MaxSubscriptionsExceeded, + ServerShutdown, + MsgHeaderViolation, + NoRespondersRequiresHeaders, +} + +public static class ClientClosedReasonExtensions +{ + public static string ToReasonString(this ClientClosedReason reason) => reason switch + { + ClientClosedReason.None => "", + ClientClosedReason.ClientClosed => "Client Closed", + ClientClosedReason.AuthenticationTimeout => "Authentication Timeout", + ClientClosedReason.AuthenticationViolation => "Authorization Violation", + ClientClosedReason.TlsHandshakeError => "TLS Handshake Error", + ClientClosedReason.SlowConsumerPendingBytes => "Slow Consumer (Pending Bytes)", + ClientClosedReason.SlowConsumerWriteDeadline => "Slow Consumer (Write Deadline)", + ClientClosedReason.WriteError => "Write Error", + ClientClosedReason.ReadError => "Read Error", + ClientClosedReason.ParseError => "Parse Error", + ClientClosedReason.StaleConnection => "Stale Connection", + ClientClosedReason.ProtocolViolation => "Protocol Violation", + ClientClosedReason.MaxPayloadExceeded => "Maximum Payload Exceeded", + ClientClosedReason.MaxSubscriptionsExceeded => "Maximum Subscriptions Exceeded", + ClientClosedReason.ServerShutdown => "Server Shutdown", + ClientClosedReason.MsgHeaderViolation => "Message Header Violation", + ClientClosedReason.NoRespondersRequiresHeaders => "No Responders Requires Headers", + _ => reason.ToString(), + }; +} diff --git a/tests/NATS.Server.Tests/ClientClosedReasonTests.cs b/tests/NATS.Server.Tests/ClientClosedReasonTests.cs new file mode 100644 index 0000000..16edf19 --- /dev/null +++ b/tests/NATS.Server.Tests/ClientClosedReasonTests.cs @@ -0,0 +1,24 @@ +namespace NATS.Server.Tests; + +public class ClientClosedReasonTests +{ + [Fact] + public void All_expected_close_reasons_exist() + { + // Verify all 17 enum values exist and are distinct (None + 16 named reasons) + var values = Enum.GetValues(); + values.Length.ShouldBe(17); + values.Distinct().Count().ShouldBe(17); + } + + [Theory] + [InlineData(ClientClosedReason.ClientClosed, "Client Closed")] + [InlineData(ClientClosedReason.SlowConsumerPendingBytes, "Slow Consumer (Pending Bytes)")] + [InlineData(ClientClosedReason.SlowConsumerWriteDeadline, "Slow Consumer (Write Deadline)")] + [InlineData(ClientClosedReason.StaleConnection, "Stale Connection")] + [InlineData(ClientClosedReason.ServerShutdown, "Server Shutdown")] + public void ToReasonString_returns_human_readable_description(ClientClosedReason reason, string expected) + { + reason.ToReasonString().ShouldBe(expected); + } +}