juanfont/headscale · warning · ErrAPIKeyFailedToParse

failed to parse ApiKey: invalid display prefix format

Error message

failed to parse ApiKey: invalid display prefix format

What it means

Returned by ParseAPIKeyPrefix when a display string starts with the 'hskey-api-' literal but strings.Cut fails to split on it. Because strings.HasPrefix is checked immediately before, Cut can never fail here - this branch is defensive dead code guarding against future refactors of the prefix constant. In practice you should never see it; if you do, apiKeyPrefix and the Cut separator have diverged.

Source

Thrown at hscontrol/db/api_key.go:170

		Where("id = ?", keyID).
		Update("user_id", uint(userID)).Error
}

// ParseAPIKeyPrefix extracts the database prefix from a display prefix.
// 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

View on GitHub (pinned to 565fd254d0)

Solutions

  1. If running a fork: verify apiKeyPrefix is a single constant used in both HasPrefix and Cut - they cannot mismatch in stock code.
  2. Treat hitting this error as a code regression: file a bug rather than working around it.
  3. Use the standard 12-character bare prefix or full 'hskey-api-...' display form to bypass this branch entirely.
Defensive patterns

Strategy: try-catch

Try / catch

prefix, err := db.ParseAPIKeyPrefix(display)
if err != nil {
    return fmt.Errorf("parsing API key prefix: %w", err)
}

Prevention

When it happens

Trigger: Calling ParseAPIKeyPrefix with a string that passes HasPrefix(displayPrefix, apiKeyPrefix) but where strings.Cut(displayPrefix, apiKeyPrefix) reports found=false - only possible if the two constants differ (e.g. a code edit changed one but not the other, or a custom build altered apiKeyPrefix).

Common situations: Forked/custom headscale builds where the key prefix constant was modified inconsistently; never occurs in stock builds.

Understand the failure class

Related errors


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