From 77cd9649ec3327b946e9a6a410175fa6def72675 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 23 Sep 2026 07:15:43 +0200 Subject: [PATCH] R-AM: snapshot handler and dispatch backstop answer/log untyped faults HandleSnapshotRequestV1Async only caught the three typed selection exceptions, unlike its siblings HandleShardLogRequestAsync/HandlePutAsync, which catch Exception and answer a CultNetErrorMessage. An untyped fault (e.g. a null Selection) escaped into the RUDP dispatch backstop, which swallowed it with no log - the same backstop that hid SM-6's ArgumentNullException regression for a whole batch. - HandleSnapshotRequestV1Async now catches Exception like its siblings and answers a CultNetErrorMessage. - RudpCultNetSchemaServer gets a Logger (ILogger/NullLogger, same pattern as Server/Client), and DispatchAsync's backstop always logs the fault it swallows, unwrapping DynamicInvoke's TargetInvocationException wrapper so the logged message is the handler's real exception, not the generic invocation-wrapper text. Co-Authored-By: Claude Sonnet 5 --- .../CultNetDatabaseServer.cs | 10 +++++++ .../CultNetRudpSchemaServer.cs | 29 +++++++++++++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/GameCult.Networking/CultNetDatabaseServer.cs b/src/GameCult.Networking/CultNetDatabaseServer.cs index 6b86f4aa..1a66dc2a 100644 --- a/src/GameCult.Networking/CultNetDatabaseServer.cs +++ b/src/GameCult.Networking/CultNetDatabaseServer.cs @@ -263,6 +263,16 @@ namespace GameCult.Networking { peer.SendCultNet(CultNetErrorMessage.ForReferenceOutsideTarget(ex)); } + // R-AM: an untyped fault answers like HandleShardLogRequestAsync/HandlePutAsync's own + // catch (Exception) - a CultNetErrorMessage on the wire, not silence. Before this, only + // the three typed selection exceptions were caught here; anything else propagated up into + // the dispatch backstop (CultNetRudpSchemaServer.cs), which swallowed it with no log and + // hid SM-6's ArgumentNullException regression for a whole batch. + catch (Exception ex) + { + _server.Logger.LogError($"CultNet snapshot v1 request failed: {ex.Message}"); + peer.SendCultNet(new CultNetErrorMessage { Error = ex.Message }); + } return Task.CompletedTask; } diff --git a/src/GameCult.Networking/CultNetRudpSchemaServer.cs b/src/GameCult.Networking/CultNetRudpSchemaServer.cs index f337dcd4..41a2017d 100644 --- a/src/GameCult.Networking/CultNetRudpSchemaServer.cs +++ b/src/GameCult.Networking/CultNetRudpSchemaServer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; +using GameCult.Logging; namespace GameCult.Networking { @@ -111,6 +112,17 @@ namespace GameCult.Networking private readonly ConcurrentDictionary _handlers = new(); private readonly ConcurrentDictionary _schemaPeerAdapters = new(); private bool _disposed; + private ILogger _logger = new NullLogger(); + + /// + /// Gets or sets the logger used by the server. R-AM: the dispatch backstop in DispatchAsync + /// logs every untyped fault through this instead of swallowing it silently. + /// + public ILogger Logger + { + get => _logger; + set => _logger = value ?? new NullLogger(); + } /// /// Creates a schema-v0 RUDP server. @@ -283,11 +295,13 @@ namespace GameCult.Networking // R-AG: a handler is expected to catch its own typed refusals (HandleSnapshotRequestV1Async // catches the three selection exceptions and answers cursor_invalid/selection_invalid/ - // reference_outside_target on the wire) - this catch is the backstop for whatever a + // reference_outside_target on the wire, R-AM) - this catch is the backstop for whatever a // handler does not, so an untyped exception from a malformed/hostile message can never - // reach the poll loop and stall the server for every other peer. It answers nothing on the - // wire (an exception this generic carries no typed refusal to report) and simply drops the - // one malformed dispatch, exactly as an unregistered handler already does just above. + // reach the poll loop and stall the server for every other peer. It still answers nothing + // on the wire (an exception this generic carries no typed refusal to report) and drops the + // one malformed dispatch, exactly as an unregistered handler already does just above - + // R-AM: but it always logs. A backstop that swallows silently is a compensator, and an + // untested, unlogged one hid the SM-6 ArgumentNullException regression for a whole batch. try { var result = handler.DynamicInvoke(message, new RudpCultNetSchemaServerPeer(this, delivered.Peer)); @@ -296,8 +310,13 @@ namespace GameCult.Networking await task.ConfigureAwait(false); } } - catch (Exception) + catch (Exception ex) { + // DynamicInvoke wraps a synchronously-thrown handler fault in TargetInvocationException, + // whose own .Message is the generic "Exception has been thrown by the target of an + // invocation." - log the real fault, not that wrapper. + var reported = ex is System.Reflection.TargetInvocationException { InnerException: { } inner } ? inner : ex; + Logger.LogError($"CultNet RUDP schema dispatch failed for {message.GetType().Name}: {reported.Message}"); return false; } return true;