Skip to content

Guard /internal/refresh per game so a feed escape cannot 500 it - #129

Merged
bgard68 merged 1 commit into
mainfrom
claude/refresh-endpoint-guard
Aug 25, 2026
Merged

Guard /internal/refresh per game so a feed escape cannot 500 it#129
bgard68 merged 1 commit into
mainfrom
claude/refresh-endpoint-guard

Conversation

@bgard68

@bgard68 bgard68 commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Twice now a feed has thrown a type RefreshGame's catch filter did not match — XmlException from MegaMillions (#117), then plain ArgumentException from Draw.Create (#126) — and both times this endpoint answered 500. It is what the keep-alive workflow calls, so a 500 reads as the whole instance being down.

Each was fixed at its source. The endpoint itself still had no guard, so the next escape would do exactly the same thing. This closes the class rather than the instances.

Per game, not around the loop

The unguarded loop aborted on the first throw, so every game after the failing one was silently skipped — a second, quieter bug in the same code.

The catch is not a swallow

catch (Exception ex) when (ex is not OperationCanceledException)
{
    logger.LogError(ex, "{Game}: refresh failed, reporting it as a feed error.", game);
    results.Add(new RefreshResult(game, UpToDate: false, NewDraws: 0,
        SkippedInvalid: 0, JackpotUpdated: false, FeedError: ex.Message));
}

It logs the exception with its game, and records a RefreshResult carrying the message as feedError — the same shape the response already uses for the failures RefreshGame does anticipate. Callers get a 200 with the error named per game, which is the existing contract, not a new one.

OperationCanceledException is deliberately excluded: a shutdown or a disconnected client is not a feed failure and must keep propagating.

Tests

Two, both verified to fail without the guard before being accepted:

  • AnUnanticipatedFeedFailure_IsReported_NotA500 — a feed throwing NotSupportedException (a type nothing in the chain anticipates) yields 200 with the error reported.
  • OneGameFailing_DoesNotCostTheOtherItsRefresh — both games still appear, and the healthy one has no feedError.

321 tests pass, dotnet build -warnaserror with zero warnings.

🤖 Generated with Claude Code

…0 it

Twice now a feed has thrown a type RefreshGame's catch filter did not match -
XmlException from MegaMillions, then plain ArgumentException from Draw.Create -
and both times this endpoint answered 500. It is what the keep-alive workflow
calls, so a 500 reads as the whole instance being down.

Each of those was fixed at its source, but the endpoint itself had no guard, so
the next escape would do the same thing. This closes the class rather than the
instances.

Per game, not around the loop: the unguarded loop aborted on the first throw,
so every game after the failing one was silently skipped.

The catch is not a swallow - it logs the exception with its game, and records a
RefreshResult carrying the message as feedError, which is the same shape the
response already uses for the failures RefreshGame does anticipate. Callers see
a 200 with the error named per game. OperationCanceledException is deliberately
excluded: a shutdown or a disconnected client is not a feed failure and must
keep propagating.

Two tests, both verified to fail without the guard: an unanticipated exception
type is reported rather than 500ing, and one game failing does not cost the
other its refresh. 321 tests pass.

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