juanfont/headscale · error
tag should be lowercase
Error message
tag should be lowercase
What it means
Error from submitConfirmForm when the OIDC confirmation interstitial HTML does not contain an action=" attribute. The function scrapes the rendered form to POST the confirmation, so a missing action attribute means the page is not the expected form (wrong page, error page, or template change).
Source
Thrown at hscontrol/api/v1/tags.go:12
package apiv1
import (
"errors"
"strings"
)
// ACL tag validation, shared by the node and pre-auth-key resources. These
// reproduce the gRPC validateTag checks and messages.
var (
errTagMissingPrefix = errors.New("tag must start with the string 'tag:'")
errTagNotLowercase = errors.New("tag should be lowercase")
errTagHasSpaces = errors.New("tags must not contain spaces")
)
// validateTag reports whether an ACL tag is well formed: it must start with
// "tag:", be lowercase, and contain no spaces.
func validateTag(tag string) error {
switch {
case !strings.HasPrefix(tag, "tag:"):
return errTagMissingPrefix
case strings.ToLower(tag) != tag:
return errTagNotLowercase
case len(strings.Fields(tag)) > 1:
return errTagHasSpaces
default:
return nil
}
}
View on GitHub (pinned to 565fd254d0)
Solutions
- Log the passed htmlBody to identify which page was actually returned.
- Check headscale OIDC logs for why the interstitial was not rendered (failed state/nonce validation).
- If you modified templates, ensure the form keeps action="..." with a double-quoted attribute.
- Ensure the same HTTP client (with CSRF cookie) is used for the callback and the confirm POST.
Defensive patterns
Strategy: validation
Validate before calling
if !strings.Contains(htmlBody, `action="`) {
log.Printf("unexpected interstitial page: %.500s", htmlBody)
return fmt.Errorf("%s page is not the confirmation form", hostname)
} Type guard
func isConfirmForm(htmlBody string) bool {
return strings.Contains(htmlBody, `action="`) && strings.Contains(htmlBody, "csrf")
} Prevention
- Log the HTML body whenever interstitial scraping fails.
- Keep headscale templates and this scraper in sync — the double-quoted action attribute is load-bearing.
- Reuse the same cookied HTTP client for callback and confirm POST so the interstitial is actually served.
When it happens
Trigger: submitConfirmForm receiving HTML that is not the confirmation interstitial — e.g. an error page or login page was rendered instead, or a headscale template change removed/renamed the action attribute.
Common situations: OIDC callback rendering an error instead of the confirmation form, headscale version drift changing the interstitial markup, or a session/cookie mismatch causing re-render of a different page.
Related errors
- test pattern is required as first argument or use --test fla
- not confirmed, aborting
- tag must start with the string 'tag:'
- unexpected end of container wait
- auth request rejected
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/8708a7e06bc3c134.
Report an issue: GitHub.