hub v0.38.1: offsite provisioning must survive a client disconnect (live F1)

First supervised live run: the ~25s spinner-less offsite save invited a
re-click; the abandoned first request's r.Context() was canceled between
CreateSubaccount and SaveOneTimeSecret, stranding sub-account 268985 with a
password lost forever (consume 404s permanently).

applyOffsite now provisions on context.WithoutCancel + 3-minute absolute
timeout: once the create starts, create->wait->store runs to completion.
Regression test with a ctx-honoring fake that cancels the request context
mid-create; red-proofed against the raw-ctx pre-fix shape (reproduces the
exact live error).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-09 21:00:27 +02:00
parent 0a65f2be5f
commit 7c70c545d9
3 changed files with 113 additions and 0 deletions
+7
View File
@@ -817,6 +817,13 @@ func (s *Server) applyOffsite(ctx context.Context, r *http.Request, cfg *store.C
if s.offsite == nil {
return fmt.Errorf("offsite provisioning is not configured on this hub (no Hetzner token)")
}
// Detach from the client's request context: provisioning takes ~25s (create + wait + host-key scan) and
// an impatient re-click cancels r.Context() MID-SEQUENCE — live finding: the cancel landed between
// CreateSubaccount and SaveOneTimeSecret, stranding a sub-account whose one-time password was lost
// forever. Once provisioning starts it must run to completion (create→wait→store is the atom); the
// absolute timeout still bounds a hung Hetzner call.
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 3*time.Minute)
defer cancel()
in := offsite.Input{
Enabled: true,
Type: strings.TrimSpace(r.FormValue("offsite_type")),