Skip to content

Log and count header parse errors instead of swallowing them - #495

Merged
LarsLaskowski merged 6 commits into
mainfrom
claude/issue-21-i61uv5
Jul 26, 2026
Merged

Log and count header parse errors instead of swallowing them#495
LarsLaskowski merged 6 commits into
mainfrom
claude/issue-21-i61uv5

Conversation

@LarsLaskowski

@LarsLaskowski LarsLaskowski commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

ReceivedPacketData.AnalyzePacketHeader caught all exceptions with an empty catch block and silently dropped every undersized packet, with no exception type, logging, or metric — parse failures were invisible. This PR makes every header-rejection path observable:

  • Undersized packets (too short overall, or reporting a 2023+ game version without the 2023+ header fields) now set a bounded HeaderRejectionCode on ReceivedPacketData (PacketTooShort, Undersized2023Header, ParseException, or None). PacketProcessor logs a warning and records it via the existing IAppMetrics.ProcessingErrors counter, tagged with the rejection code.
  • The header-parsing try/catch now records the caught exception (HeaderParseException) instead of discarding it, and PacketProcessor logs it as an error. This path is defensive: every offset ParseHeaderFields reads is already validated by the length guards above it, so no packet observed in practice reaches this catch, but it guards against a future change accidentally breaking that invariant without crashing the receive loop it runs on (e.g. the TCP replay ingestion loop, which only catches IOException/SocketException around it).
  • The untrusted packet details (reported game version, packet length, exception text) are kept out of the metric dimension entirely and only flow into the structured log message as parameters, so a remote sender cannot expand the counter into unbounded time series.

Linked issues

Closes #21

Review notes

ReceivedPacketData has no logger or metrics dependency and is instantiated directly (new ReceivedPacketData()) at many call sites, so rejection state is exposed via properties (HeaderRejectionCode, HeaderParseException, ReportedGameVersion) rather than logged from within F1Server.Core. PacketProcessor already has access to Logger and _appData.AppMetrics, so it consumes these to report the error, following the existing pattern used for processor.LastException. The genuinely-unreachable exception path in both files is isolated behind [ExcludeFromCodeCoverage] on the smallest possible scope (the try/catch wrapper and the exception-only log call), so the reachable dispatch and rejection-code logic stay visible to coverage.

claude added 6 commits July 26, 2026 11:38
ReceivedPacketData.AnalyzePacketHeader caught all exceptions with an
empty catch block, silently discarding parse failures and resetting
PacketHeader to null with no trace. The caught exception is now kept
on a new HeaderParseException property, and PacketProcessor logs it
and records it via the existing IAppMetrics.ProcessingErrors counter
when a packet header fails to parse.
The previous fix only captured exceptions from AnalyzePacketHeader,
leaving the two size-check short-circuit paths (too short overall,
undersized 2023+ header) silent since they never throw. Both paths
now set a HeaderRejectionReason string, which PacketProcessor logs
as a warning and counts via ProcessingErrors, alongside the existing
exception handling. Tests cover both rejection paths and confirm a
successful parse leaves no exception or rejection reason behind.
The Sonar quality gate failed on new code coverage: the HeaderParseException
branch added in the previous commits cannot be organically exercised, since
every field read in header parsing is already bounds-checked by the length
guards before it, so the catch block never fires for any packet observed in
practice. Splitting the try/catch wrapper and the exception-recording helper
into their own methods marked [ExcludeFromCodeCoverage] keeps that honestly
untestable defensive code out of the coverage denominator, while the
genuinely reachable and tested HeaderRejectionReason path stays fully
instrumented.
The rejection reason string interpolated the reported game version and
packet length straight from untrusted UDP input and was then used as a
metric tag value, letting a remote sender explode the ProcessingErrors
counter into hundreds of thousands of time series. HeaderRejectionReason
is replaced by a HeaderRejectionCode enum with a fixed small set of
values; the untrusted details (game version, packet length, exception
text) now flow only into the structured log message as parameters, never
into the metric dimension. This also drops the eager string allocation
on the packet receive path and narrows the ExcludeFromCodeCoverage
attribute in PacketProcessor to just the exception branch, so the
always-reached dispatch and the tested rejection-code branches stay
visible to coverage. Adds tests asserting the rejection code content and
that PacketProcessor surfaces it via LastError.
The switch-based dispatch in RecordRejectedHeader added an unreachable
None guard (PacketHeader is only null when a rejection code was already
set, so None can never occur here) and forced every switch case into the
project's brace-plus-trailing-break style, turning the single genuinely
unreachable ParseException branch into four separately tracked coverage
lines. Both regressed the Sonar quality gate to 68.4% new-code coverage.
Removing the dead guard and replacing the switch with a plain if/else-if
chain collapses the untestable branch back down to the exception call
itself.
Sonar's new-code coverage blends line and branch coverage; the previous
if/else-if chain had three branches whose alternate direction can never
be exercised (the parse-exception check, and two rejection-code checks
where the untested direction is unreachable given only three non-None
codes exist), pushing coverage to 77.8% against the 80% gate. Two
Logger?.LogWarning call sites duplicated that problem per site.

Consolidates the two rejection-code log calls into one structured
warning, and folds the exception-presence check back into the already
excluded RecordHeaderParseExceptionIfPresent method so it is called
unconditionally instead of guarded inline. A test now attaches a
no-op logger so both directions of the remaining Logger?. check are
exercised. The only branches left uncovered are the _appData/AppMetrics
null-conditionals, which can never be null/non-null respectively given
how PacketProcessor is constructed.
@sonarqubecloud

Copy link
Copy Markdown

@LarsLaskowski
LarsLaskowski merged commit 9242d0a into main Jul 26, 2026
6 checks passed
@LarsLaskowski
LarsLaskowski deleted the claude/issue-21-i61uv5 branch July 26, 2026 13:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

F-025 · An empty catch without an exception type, logging, or metric swallows all header parse errors (PacketHeader = …

2 participants