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
- Log or dump `htmlBody` when this fails and confirm which page was actually served.
- Ensure the HTTP client carries the cookies/redirect history from the registration flow so the confirm form is reached.
- If the template was updated, align the literal `name="headscale_register_confirm"` in integration/scenario.go with the new field name.
- 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
- Reuse the same cookie-bearing http.Client for the GET and the POST so the session matches.
- Dump the body on mismatch — nine times out of ten it is a different page than assumed.
- Rename the constant in one place shared by template and parser.
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
- %s confirm form: unterminated action attribute
- %s confirm form: no input tag for CSRF
- %s confirm form: unterminated input tag
- %s confirm form: no value in CSRF input
- %s confirm returned status %d: %s
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/0eb99653a3b5ead5.
Report an issue: GitHub.