juanfont/headscale · error
tag must start with the string 'tag:'
Error message
tag must start with the string 'tag:'
What it means
Error (with body and redirectURL returned) from doLoginURLWithClient when the response status is >= 400 (Bad Request and above), regardless of followRedirects. The login endpoint itself answered with a client or server error; body is logged and returned for diagnosis.
Source
Thrown at hscontrol/api/v1/tags.go:11
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
- Inspect the logged response body — providers usually include a reason.
- If 400/410-ish, the login URL may be single-use: regenerate it rather than replaying.
- Check headscale logs for a 5xx stack trace and fix the underlying handler error.
- Verify the hostname/network used to reach the login URL matches what issued it.
Defensive patterns
Strategy: try-catch
Try / catch
body, redir, err := doLoginURLWithClient(hostname, loginURL, hc, true)
if err != nil && strings.Contains(err.Error(), "unexpected status code") {
// 4xx/5xx: body is populated — surface it in the test failure message
t.Logf("login error body: %s", body)
} Prevention
- Treat login URLs as single-use; regenerate rather than replay.
- Correlate with control-server logs when 5xx appears — it is a server-side defect.
- Never silently swallow the returned body on this error.
When it happens
Trigger: GET on the login URL returning 4xx/5xx — e.g. 400 from a malformed auth request, 401 from an expired/invalid login session, or 500 from a control-server handler panic.
Common situations: Login URL already consumed or expired (interactive login attempted twice), headscale handler erroring (DB failure, policy error), or an OIDC provider returning 4xx for a bad callback.
Related errors
- not confirmed, aborting
- test pattern is required as first argument or use --test fla
- unexpected end of container wait
- auth request rejected
- tag should be lowercase
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/87607dc325bbff35.
Report an issue: GitHub.