juanfont/headscale · error

%s confirm form: no input tag for CSRF

Error message

%s confirm form: no input tag for CSRF

What it means

The parser found the CSRF field name (`name="headscale_register_confirm"`) but no `<input` tag opening before it in the preceding HTML. It walks backwards with `strings.LastIndex(before, "<input")` to capture the whole tag; failure means the attribute exists outside an input element (or the markup around it is not what the template normally emits).

Source

Thrown at integration/scenario.go:1277

	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
	}

	valStart := valIdx + len(`value="`)
	valEnd := strings.Index(inputTag[valStart:], `"`)
	csrfToken := inputTag[valStart : valStart+valEnd]

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Inspect the HTML preceding the CSRF field name to see what element actually carries it.
  2. Make the search case-insensitive (`strings.LastIndex(strings.ToLower(before), "<input")`) if only casing changed.
  3. Switch to x/net/html form parsing so element structure is handled properly.
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: Produced when the literal `name="headscale_register_confirm"` appears in the body but the text before it contains no `<input` — e.g. the name is rendered inside a <label>, a script string, or the tag is emitted as `<INPUT` with different casing.

Common situations: Template refactor moved the hidden input into a partial that renders different markup; attribute appears in an error message echoed to the page; case change in the tag name.

Related errors


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