diff --git a/internal/web/actions.go b/internal/web/actions.go index 7a41e3b..a95ef96 100644 --- a/internal/web/actions.go +++ b/internal/web/actions.go @@ -158,7 +158,8 @@ func (a *Actions) statusRows(ctx context.Context, rt *actionRuntime) (map[alertR if !rt.fetched.IsZero() && a.now().Sub(rt.fetched) < statusCacheTTL { return rt.rows, rt.err } - rows, err := a.fetchStatus(ctx, rt.cfg) + // A client that disconnects mid-request must not fail the fetch that every later request reuses. + rows, err := a.fetchStatus(context.WithoutCancel(ctx), rt.cfg) if err != nil { a.log.Warn("action status unavailable", "action", rt.cfg.ID, "err", err) } diff --git a/internal/web/actions_test.go b/internal/web/actions_test.go index 522d566..29910d8 100644 --- a/internal/web/actions_test.go +++ b/internal/web/actions_test.go @@ -2,6 +2,7 @@ package web import ( "bytes" + "context" "encoding/json" "io" "log/slog" @@ -407,3 +408,15 @@ func TestRedirectDoesNotForwardToken(t *testing.T) { t.Fatal("bearer token forwarded across a redirect") } } + +func TestStatusFetchSurvivesCancelledRequest(t *testing.T) { + fh := newFakeHook(t) + fh.statBody = `[{"repo":"o/r","url":"` + alertURL + `","state":"running"}]` + h := newActionServer(t, nil, fh.srv.URL) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + rows, err := h.actions.statusRows(ctx, h.actions.list[0]) + if err != nil || len(rows) != 1 { + t.Fatalf("rows=%d err=%v, want the fetch to complete despite the cancelled request", len(rows), err) + } +}