juanfont/headscale · warning · ErrAPIKeyFailedToParse
failed to parse ApiKey: prefix contains invalid characters
Error message
failed to parse ApiKey: prefix contains invalid characters
What it means
Returned by ParseAPIKeyPrefix when the first 12 characters after 'hskey-api-' fail the isValidBase64URLSafe check. Headscale generates prefixes from a base64 URL-safe alphabet, so invalid characters (spaces, '+', '/', control chars, shell-expanded symbols) mean the string is not a headscale-generated key.
Source
Thrown at hscontrol/db/api_key.go:182
// If it starts with the API key prefix, parse it
if strings.HasPrefix(displayPrefix, apiKeyPrefix) {
// Remove the "hskey-api-" prefix
_, remainder, found := strings.Cut(displayPrefix, apiKeyPrefix)
if !found {
return "", fmt.Errorf("%w: invalid display prefix format", ErrAPIKeyFailedToParse)
}
// Extract just the first 12 characters (the actual prefix)
if len(remainder) < apiKeyPrefixLength {
return "", fmt.Errorf("%w: prefix too short", ErrAPIKeyFailedToParse)
}
prefix := remainder[:apiKeyPrefixLength]
// Validate it's base64 URL-safe
if !isValidBase64URLSafe(prefix) {
return "", fmt.Errorf("%w: prefix contains invalid characters", ErrAPIKeyFailedToParse)
}
return prefix, nil
}
// For legacy 7-character prefixes or other formats, return as-is
return displayPrefix, nil
}
// validateAPIKey validates an API key and returns the key if valid.
// Handles both new (hskey-api-{prefix}-{secret}) and legacy (prefix.secret) formats.
func validateAPIKey(db *gorm.DB, keyStr string) (*types.APIKey, error) {
// Validate input is not empty
if keyStr == "" {
return nil, ErrAPIKeyFailedToParse
}
// Check for new format: hskey-api-{prefix}-{secret}View on GitHub (pinned to 565fd254d0)
Solutions
- Re-copy the key verbatim from its source ('headscale apikeys create' output) without shell interpolation.
- Quote the key in shell usage: use single quotes to prevent variable expansion.
- If the key was stored URL-encoded, decode it once before passing.
Example fix
// before _, err := db.ParseAPIKeyPrefix(os.Args[1]) // unquoted $ or ! chars got expanded // after // pass the key single-quoted on the command line: // headscale ... --apikey 'hskey-api-XXXXXXXXXXXX-YYYY...' _, err := db.ParseAPIKeyPrefix(key)
Defensive patterns
Strategy: validation
Validate before calling
var base64urlRe = regexp.MustCompile(`^[A-Za-z0-9_-]{12,}$`)
if strings.HasPrefix(display, "hskey-api-") && !base64urlRe.MatchString(strings.TrimPrefix(display, "hskey-api-")) {
return errors.New("API key contains invalid characters; re-copy it verbatim")
} Type guard
func looksLikeBase64URLSafe(s string) bool {
for _, r := range s {
if !(r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-' || r == '_') {
return false
}
}
return true
} Try / catch
if _, err := db.ParseAPIKeyPrefix(display); err != nil && errors.Is(err, db.ErrAPIKeyFailedToParse) {
// input is not a headscale-generated key; do not retry, re-collect the value
} Prevention
- Single-quote API keys in shell commands to prevent $ / ! expansion.
- Pass keys via environment variables or files instead of command-line arguments.
- Do not URL-encode/decode headscale keys; they are already URL-safe.
When it happens
Trigger: Calling ParseAPIKeyPrefix with 'hskey-api-' followed by characters outside [A-Za-z0-9_-]: a key corrupted by shell expansion ($, !), URL-decoded with '+' or '/' inside, or a foreign token pasted with the headscale prefix.
Common situations: Keys pasted through terminals that mangle special characters; double-URL-encoding/decoding; users prefixing another system's token with 'hskey-api-'.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- failed to parse ApiKey: prefix too short
- failed to parse ApiKey
- failed to parse ApiKey: invalid display prefix format
- HEADSCALE_CLI_API_KEY environment variable needs to be set
- failed to generate API key
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/07cf263aebac401c.
Report an issue: GitHub.