juanfont/headscale · error

%s confirm form: no CSRF input

Error message

%s confirm form: no CSRF input

What it means

The confirm-form parser could not find the hidden CSRF input `name="headscale_register_confirm"` anywhere in the HTML body. The integration Scenario auto-submits the registration confirm form and needs this hidden field's value to POST; its absence means the page returned is not the expected confirm form.

Source

Thrown at integration/scenario.go:1272

	actionIdx := strings.Index(htmlBody, `action="`)
	if actionIdx == -1 {
		return "", nil, fmt.Errorf("%s confirm form: no action attribute", hostname) //nolint:err113
	}

	actionStart := actionIdx + len(`action="`)

	actionEnd := strings.Index(htmlBody[actionStart:], `"`)
	if actionEnd == -1 {
		return "", nil, fmt.Errorf("%s confirm form: unterminated action attribute", hostname) //nolint:err113
	}

	formAction := htmlBody[actionStart : actionStart+actionEnd]

	// Extract hidden CSRF input value. The rendered <input> has
	// attributes in name-type-value order so we grab the whole tag.
	before, _, ok := strings.Cut(htmlBody, `name="headscale_register_confirm"`)
	if !ok {
		return "", nil, fmt.Errorf("%s confirm form: no CSRF input", hostname) //nolint:err113
	}

	tagStart := strings.LastIndex(before, "<input")
	if tagStart == -1 {
		return "", nil, fmt.Errorf("%s confirm form: no input tag for CSRF", hostname) //nolint:err113
	}

	tagEnd := strings.Index(htmlBody[tagStart:], ">")
	if tagEnd == -1 {
		return "", nil, fmt.Errorf("%s confirm form: unterminated input tag", hostname) //nolint:err113
	}

	inputTag := htmlBody[tagStart : tagStart+tagEnd+1]

	valIdx := strings.Index(inputTag, `value="`)
	if valIdx == -1 {
		return "", nil, fmt.Errorf("%s confirm form: no value in CSRF input", hostname) //nolint:err113
	}

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Log or dump `htmlBody` when this fails and confirm which page was actually served.
  2. Ensure the HTTP client carries the cookies/redirect history from the registration flow so the confirm form is reached.
  3. If the template was updated, align the literal `name="headscale_register_confirm"` in integration/scenario.go with the new field name.
  4. Check that the registration key/auth-id is still valid at this point in the flow.
Defensive patterns

Strategy: validation

Validate before calling

want := `name="headscale_register_confirm"`
if !strings.Contains(htmlBody, want) {
    log.Printf("%s unexpected confirm page (first 500 bytes): %.500s", hostname, htmlBody)
}

Prevention

When it happens

Trigger: The helper runs `strings.Cut(htmlBody, "name=\"headscale_register_confirm\"")` after a successful GET of the confirm page and `ok` is false. Produced when the server returned a login/error/redirect page instead of the confirm form, or when the template renamed the field.

Common situations: Auth flow changed (OIDC interposed, session cookie missing so an intermediate page renders); the hidden input's attribute order or quoting changed in the template; the registration key was already consumed so a 'node already registered' page is returned.

Related errors


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