juanfont/headscale · error · parseErr (ErrPreAuthKeyFailedToParse|ErrOAuthClientFailedToParse|ErrAccessTokenFailedToParse|ErrAPIKeyFailedToParse)

%w: expected separator '-' at position %d, got '%c'

Error message

%w: expected separator '-' at position %d, got '%c'

What it means

parsePrefixedKey found no '-' separator at the expected position (character index prefixLen, i.e. position 12 in current keys). The new key format is fixed-layout: prefix, then '-', then secret. Because the secret itself may contain dashes, the separator must be exactly at prefixLen; anything else is malformed.

Source

Thrown at hscontrol/db/preauth_keys.go:258

	//nolint:unparam // kept explicit though every credential kind uses a 12-char prefix and 64-char secret today
	prefixLen, secretLen int,
	parseErr error,
) (string, string, error) {
	expectedMinLength := prefixLen + 1 + secretLen
	if len(prefixAndSecret) < expectedMinLength {
		return "", "", fmt.Errorf(
			"%w: key too short, expected at least %d chars after prefix, got %d",
			parseErr,
			expectedMinLength,
			len(prefixAndSecret),
		)
	}

	prefix := prefixAndSecret[:prefixLen]

	// Validate separator at expected position
	if prefixAndSecret[prefixLen] != '-' {
		return "", "", fmt.Errorf(
			"%w: expected separator '-' at position %d, got '%c'",
			parseErr,
			prefixLen,
			prefixAndSecret[prefixLen],
		)
	}

	secret := prefixAndSecret[prefixLen+1:]

	// Validate secret length
	if len(secret) != secretLen {
		return "", "", fmt.Errorf(
			"%w: secret length mismatch, expected %d chars, got %d",
			parseErr,
			secretLen,
			len(secret),
		)
	}

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Regenerate the key and pass it through unmodified.
  2. If assembling programmatically, use fmt.Sprintf("%s-%s", prefix, secret) with a 12-char prefix and 64-char secret.
  3. Check for accidental character insertion/deletion around the 13th character after 'hskey-<kind>-'.
Defensive patterns

Strategy: validation

Validate before calling

func hasSeparatorAtPosition(rest string, pos int) bool {
    return len(rest) > pos && rest[pos] == '-'
}

Prevention

When it happens

Trigger: Submitting a key where the prefix is not exactly 12 characters (extra or missing character shifts the separator), a key assembled by concatenating parts with the wrong delimiter, or a hand-typed key with a typo in the prefix.

Common situations: Scripts that join prefix and secret with '_' or no dash; users deleting a character from the prefix during copy; mixed-format keys from older versions with different prefix lengths.

Related errors


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