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

  1. Read the response body embedded in the error message — it names the actual server-side reason.
  2. 403/CSRF: re-run the whole fetch-then-submit sequence without delay in between.
  3. 500: check headscale logs for the stack trace and fix the underlying registration failure (missing user, DB error).
  4. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/777a27175a78b577. Report an issue: GitHub.