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

  1. Inspect the logged response body — providers usually include a reason.
  2. If 400/410-ish, the login URL may be single-use: regenerate it rather than replaying.
  3. Check headscale logs for a 5xx stack trace and fix the underlying handler error.
  4. 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

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


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