Skip to content

Fix /internal/refresh 500: catch ArgumentException, not just its subclass - #126

Merged
bgard68 merged 1 commit into
mainfrom
claude/fix-refresh-500
Aug 25, 2026
Merged

Fix /internal/refresh 500: catch ArgumentException, not just its subclass#126
bgard68 merged 1 commit into
mainfrom
claude/fix-refresh-500

Conversation

@bgard68

@bgard68 bgard68 commented Aug 25, 2026

Copy link
Copy Markdown
Owner

The post-deploy smoke test went red on main after #117 merged — /internal/refresh answered 500. The deploy itself succeeded and 32 of 33 smoke checks passed; only the keyed refresh failed.

My fix in #117 was incomplete. Two escapes remained.

1. ArgumentOutOfRangeException is a subclass of ArgumentException

Draw.Create throws plain ArgumentException for a row that is not five distinct balls:

if (whites.Length != 5)
    throw new ArgumentException($"Expected 5 white balls, got {whites.Length}.", nameof(whiteBalls));
if (whites.Distinct().Count() != 5)
    throw new ArgumentException("White balls must be distinct.", nameof(whiteBalls));

#117's guard caught ArgumentOutOfRangeException, which derives from ArgumentException — so it caught the narrow type and let the base type through. And the base type is exactly what the Socrata feed produces in the minutes after a drawing, when the row is published with placeholder numbers.

The timing fits: the smoke test ran at 23:03 ET on a Monday, four minutes after the 22:59 Powerball drawing. The deploy seven minutes earlier, on the same workflow, passed.

2. RuleEras.ForDate throws rather than returning a violation

EraValidator.Validate calls RuleEras.ForDate, which throws ArgumentOutOfRangeException for a date no era covers instead of returning a violation. That call sits inside RefreshGame's feed loop, but its exception matched none of the filter's types, so a single bogus date escaped ExecuteAsync entirely. /internal/refresh has no try/catch of its own, so the request 500s and the game after the failing one never refreshes.

Both paths now report a feed error, which is what RefreshGame's own summary documents: "Feed failures are reported, never thrown — a broken external source must not take the app down."

Tests

Three regression tests, each verified to fail against the previous code before being accepted:

  • APlaceholderRow_IsReportedAsAFeedError_NotThrownAtTheCaller — two placeholder row shapes ("00 00 00 00 00 00" and a repeated ball), through the real Socrata feed with a canned response.
  • ADrawDatedBeforeAnyKnownEra_IsReportedAsAFeedError_NotThrown — an out-of-era draw date through RefreshGame, asserting FeedError is set rather than an exception escaping.

319 tests pass (316 + 3), dotnet build -warnaserror with zero warnings, locked-mode restore clean.

Note on scope

The batch is still refused rather than silently delivered short — a partial batch would let gap-repair skip real draws. The change is only in how the failure surfaces: as a reported feedError on a 200, not an unhandled exception on a 500.

🤖 Generated with Claude Code

The post-deploy smoke test went red on main: /internal/refresh answered 500.
My previous fix was incomplete in two places, and this is the case it missed.

Draw.Create throws plain ArgumentException for a row that is not five distinct
balls. ArgumentOutOfRangeException DERIVES from ArgumentException, so catching
only the derived type let the base type straight through - and the base type is
what the Socrata feed produces in the minutes after a drawing, when the row is
published with placeholder numbers. The smoke test ran at 23:03 ET on a Monday,
four minutes after the 22:59 Powerball draw.

Second escape, in RefreshGame itself: EraValidator.Validate calls
RuleEras.ForDate, which THROWS ArgumentOutOfRangeException for a date no era
covers rather than returning a violation. That call sits inside the feed loop
but its exception matched none of the filter's types, so a single bogus date
escaped ExecuteAsync. /internal/refresh has no try/catch, so it 500s and the
game after the failing one never refreshes.

Both now report a feed error, which is what this class documents.

Three regression tests, each verified to fail against the previous code:
two placeholder-row shapes through the Socrata feed, and an out-of-era draw
date through RefreshGame. 319 tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWkVh7cyAz1gWapBH1CY8n
@bgard68
bgard68 merged commit f15cd20 into main Aug 25, 2026
7 checks passed
@bgard68
bgard68 deleted the claude/fix-refresh-500 branch August 25, 2026 03:11
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.

2 participants