juanfont/headscale · warning · ErrAPIKeyFailedToParse

failed to parse ApiKey: prefix too short

Error message

failed to parse ApiKey: prefix too short

What it means

Returned by ParseAPIKeyPrefix when the input begins with 'hskey-api-' but the remainder after the prefix is shorter than apiKeyPrefixLength (12 characters). The function needs at least 12 chars to extract the database lookup prefix, so a truncated or hand-mangled key string triggers it.

Source

Thrown at hscontrol/db/api_key.go:175

// Handles formats: "hskey-api-{12chars}-***", "hskey-api-{12chars}", or just "{12chars}".
// Returns the 12-character prefix suitable for database lookup.
func ParseAPIKeyPrefix(displayPrefix string) (string, error) {
	// If it's already just the 12-character prefix, return it
	if len(displayPrefix) == apiKeyPrefixLength && isValidBase64URLSafe(displayPrefix) {
		return displayPrefix, nil
	}

	// 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.

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Re-copy the full API key - the display form must be 'hskey-api-' + 12+ chars.
  2. If passing a bare prefix, pass exactly the 12-character prefix without the 'hskey-api-' decoration.
  3. Audit scripts that generate or transform key strings for truncation.

Example fix

// before
prefix, err := db.ParseAPIKeyPrefix(truncatedKey)

// after
if strings.HasPrefix(k, "hskey-api-") && len(k) < len("hskey-api-")+12 {
    return errors.New("API key was truncated; re-copy the full key")
}
prefix, err := db.ParseAPIKeyPrefix(k)
Defensive patterns

Strategy: validation

Validate before calling

const minDisplayLen = len("hskey-api-") + 12 // apiKeyPrefixLength
if strings.HasPrefix(display, "hskey-api-") && len(display) < minDisplayLen {
    return errors.New("API key truncated: expected at least 12 chars after the prefix")
}
prefix, err := db.ParseAPIKeyPrefix(display)

Type guard

func isPlausibleDisplayKey(s string) bool {
    return !strings.HasPrefix(s, "hskey-api-") || len(s) >= len("hskey-api-")+12
}

Try / catch

if _, err := db.ParseAPIKeyPrefix(display); err != nil {
    if errors.Is(err, db.ErrAPIKeyFailedToParse) {
        // reject the input, prompt the user to re-copy the key
    }
}

Prevention

When it happens

Trigger: Calling ParseAPIKeyPrefix (or validateAPIKey with a display key) with inputs like 'hskey-api-', 'hskey-api-abc', or a key copied incompletely (truncated by terminal width, clipboard, or log truncation).

Common situations: Users copy-paste an API key that was cut off; scripts that build keys by string concatenation with an empty secret; shell history expansion mangling the key.

Understand the failure class

Related errors


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