fix(api): report the classified cause of a failed run on /api/v1/stats - #1301
Merged
Merged
Conversation
krisarmstrong
enabled auto-merge
September 16, 2026 19:07
The UI declared Stats.errorMessage and ReflectorPage renders it as the failure headline, but the Go struct /api/v1/stats serialises had no such field: the four statusError sites logged the cause server-side only, so a bind conflict, a missing capability and a vanished interface all reached the operator as the generic "stopped with an error". internal/api/run_error.go classifies the failure into a closed set of five sentences and never echoes the raw error, which names interfaces, socket paths and peer addresses -- the same no-leak policy respondTestExecutionError already applied to TestResultResponse.Error. Server.testError is set at all four sites, cleared when the next run begins so a stale cause cannot describe a run that is not failing, and mirrored by snapshotStats. check-types-drift.sh could not have caught this: it diffs the generated TS tree against the committed schemas, and Stats lives in the hand-written api.ts, which nothing compares to the Go struct. runModuleTest's failure branch turns out to have no production caller. Both are rowed as D-STEM-13 rather than widened into this change. Fixes #1251
krisarmstrong
force-pushed
the
fix/d-stem-5-stats-error-message
branch
from
September 16, 2026 19:09
f25c617 to
3e0846f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/api/v1/statsnow carries the classified cause of a failed run, so theReflector page can say what went wrong instead of "The reflector stopped with
an error".
ui/src/types/api.ts:42declaredStats.errorMessageandReflectorPage.tsx:242rendersstats.errorMessage || t('reflector.stoppedWithError'),but the Go
Statsstruct had no such field — the fourstatusErrorsiteslogged the real cause server-side only. A bind conflict, a missing capability or
a vanished interface all looked identical to the operator, who had to read the
daemon log to diagnose the Free tier's own feature.
What the wire carries.
internal/api/run_error.gomaps the failure onto aclosed set of five sentences — interface or port busy, permission, interface
missing, peer unreachable, and a deliberately vague fallback. The raw error is
never echoed: it names interfaces, socket paths and peer addresses, which are
the daemon host's business and not stable API. That is the same no-leak policy
respondTestExecutionErroralready applied toTestResultResponse.Error; thedaemon log keeps the full text for support. Matching is on substrings because
the text arrives from three layers (Go net, the cgo dataplane, module
executors) that word the same condition differently.
Server.testErroris set at all four failure sites, cleared bybeginTestRunand
beginRunPlanso a stale cause cannot describe a run that is not failing,and mirrored into the snapshot by
snapshotStats.Why
check-types-drift.shmissed it. It regeneratesui/src/types/generated/*.tsfrom the committed schemas and diffs the result.Statsis not in that set:ui/src/types/api.tsis hand-written and nothingcompares it to the Go struct, so a TS-only field is invisible to the gate. That
is a gap, not a bug in this change — rowed as D-STEM-13 in the plan rather
than widened into this PR.
One thing the row's site list gets wrong.
runModuleTest's asynchronousfailure branch (
executors.go) has no production caller:executeTestisreached only from
startReflectorRequest, whose module is always the reflector,which returns through
executeReflectorbefore it. The bookkeeping is keptconsistent there and covered by a direct test, and the dead path is noted in
that test rather than silently propagated — also rowed (D-STEM-13) rather than
deleted here.
Linked Issue
Fixes #1251
Type of Change
Risk
Low. One additive, omitempty JSON field; no route, auth or dataplane change. The
field is empty unless a run failed, so a client that ignores it sees today's
behaviour. The only UI change is a test fixture that had invented wording the
daemon never sent.
Testing Evidence
Red first — the field did not exist:
Green, and each of the four production sites proven load-bearing by deleting
its assignment and re-running the suite (line-based deletion — an earlier
regex attempt cut the line mid-call and the resulting build failure would have
been mistaken for a caught mutant):
Gates (golangci-lint reports v2.13.2, the pinned CI version):
The acceptance itself — the field on the wire after a forced failure, and the
TS type matching the Go struct:
Security and Release Checklist
addresses stay in the daemon log (table test asserts the non-echo)
govulncheck ./...clean//nolint, nobiome-ignore