juanfont/headscale · error
%s confirm returned status %d: %s
Error message
%s confirm returned status %d: %s
What it means
The confirm POST completed but returned a non-200 status. The error embeds both the status code and the entire response body, so the server's own error text is visible — e.g. 403 from a bad/expired CSRF token, 500 from a server-side registration failure.
Source
Thrown at integration/scenario.go:1331
if err != nil {
return "", nil, fmt.Errorf("%s creating confirm request: %w", hostname, err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
confirmResp, err := hc.Do(req)
if err != nil {
return "", nil, fmt.Errorf("%s sending confirm request: %w", hostname, err)
}
defer confirmResp.Body.Close()
confirmBytes, err := io.ReadAll(confirmResp.Body)
if err != nil {
return "", nil, fmt.Errorf("%s reading confirm response: %w", hostname, err)
}
if confirmResp.StatusCode != http.StatusOK {
return string(confirmBytes), nil, fmt.Errorf( //nolint:err113
"%s confirm returned status %d: %s",
hostname, confirmResp.StatusCode, string(confirmBytes),
)
}
return string(confirmBytes), nil, nil
}
var errParseAuthPage = errors.New("parsing auth page")
func (s *Scenario) runHeadscaleRegister(userStr string, body string) error {
// see api.go HTML template
codeSep := strings.Split(body, "</code>")
if len(codeSep) != 2 {
return errParseAuthPage
}
keySep := strings.Split(codeSep[0], "--auth-id ")View on GitHub (pinned to 565fd254d0)
Solutions
- Read the response body embedded in the error message — it names the actual server-side reason.
- 403/CSRF: re-run the whole fetch-then-submit sequence without delay in between.
- 500: check headscale logs for the stack trace and fix the underlying registration failure (missing user, DB error).
- Confirm the auth key/auth-id is still valid when the confirm POST lands.
Defensive patterns
Strategy: try-catch
Try / catch
// Branch on status instead of failing blindly.
if confirmResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(confirmResp.Body)
if confirmResp.StatusCode >= 500 {
log.Printf("%s server error on confirm: %s", hostname, body) // investigate
}
return string(body), nil, fmt.Errorf("%s confirm returned status %d", hostname, confirmResp.StatusCode)
} Prevention
- Submit the confirm POST immediately after scraping the token to avoid expiry.
- Always read the embedded body — it carries the server's reason.
- Create the target user before registering nodes into it.
When it happens
Trigger: POSTing the scraped CSRF token after it expired or did not match the session; registering a node whose key was already consumed; headscale returns 500 because the user does not exist or the DB rejected the node.
Common situations: Delay between fetching and submitting the form let the token expire; the `--user` passed to the flow does not exist; policy rejects the node registration.
Related errors
- %s confirm form: no CSRF input
- unexpected end of container wait
- no docker context found
- system checks failed
- unknown --set value (want must|all)
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/777a27175a78b577.
Report an issue: GitHub.