{"record":{"id":"3fa7ab5196670c46","repo":"juanfont/headscale","slug":"failed-to-parse-apikey-prefix-too-short","errorCode":null,"errorMessage":"failed to parse ApiKey: prefix too short","messagePattern":"failed to parse ApiKey: prefix too short","errorType":"validation","errorClass":"ErrAPIKeyFailedToParse","httpStatus":500,"severity":"warning","filePath":"hscontrol/db/api_key.go","lineNumber":175,"sourceCode":"// Handles formats: \"hskey-api-{12chars}-***\", \"hskey-api-{12chars}\", or just \"{12chars}\".\n// Returns the 12-character prefix suitable for database lookup.\nfunc ParseAPIKeyPrefix(displayPrefix string) (string, error) {\n\t// If it's already just the 12-character prefix, return it\n\tif len(displayPrefix) == apiKeyPrefixLength && isValidBase64URLSafe(displayPrefix) {\n\t\treturn displayPrefix, nil\n\t}\n\n\t// If it starts with the API key prefix, parse it\n\tif strings.HasPrefix(displayPrefix, apiKeyPrefix) {\n\t\t// Remove the \"hskey-api-\" prefix\n\t\t_, remainder, found := strings.Cut(displayPrefix, apiKeyPrefix)\n\t\tif !found {\n\t\t\treturn \"\", fmt.Errorf(\"%w: invalid display prefix format\", ErrAPIKeyFailedToParse)\n\t\t}\n\n\t\t// Extract just the first 12 characters (the actual prefix)\n\t\tif len(remainder) < apiKeyPrefixLength {\n\t\t\treturn \"\", fmt.Errorf(\"%w: prefix too short\", ErrAPIKeyFailedToParse)\n\t\t}\n\n\t\tprefix := remainder[:apiKeyPrefixLength]\n\n\t\t// Validate it's base64 URL-safe\n\t\tif !isValidBase64URLSafe(prefix) {\n\t\t\treturn \"\", fmt.Errorf(\"%w: prefix contains invalid characters\", ErrAPIKeyFailedToParse)\n\t\t}\n\n\t\treturn prefix, nil\n\t}\n\n\t// For legacy 7-character prefixes or other formats, return as-is\n\treturn displayPrefix, nil\n}\n\n// validateAPIKey validates an API key and returns the key if valid.\n// Handles both new (hskey-api-{prefix}-{secret}) and legacy (prefix.secret) formats.","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/api_key.go#L157-L193","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Re-copy the full API key - the display form must be 'hskey-api-' + 12+ chars.","If passing a bare prefix, pass exactly the 12-character prefix without the 'hskey-api-' decoration.","Audit scripts that generate or transform key strings for truncation."],"exampleFix":"// before\nprefix, err := db.ParseAPIKeyPrefix(truncatedKey)\n\n// after\nif strings.HasPrefix(k, \"hskey-api-\") && len(k) < len(\"hskey-api-\")+12 {\n    return errors.New(\"API key was truncated; re-copy the full key\")\n}\nprefix, err := db.ParseAPIKeyPrefix(k)","handlingStrategy":"validation","validationCode":"const minDisplayLen = len(\"hskey-api-\") + 12 // apiKeyPrefixLength\nif strings.HasPrefix(display, \"hskey-api-\") && len(display) < minDisplayLen {\n    return errors.New(\"API key truncated: expected at least 12 chars after the prefix\")\n}\nprefix, err := db.ParseAPIKeyPrefix(display)","typeGuard":"func isPlausibleDisplayKey(s string) bool {\n    return !strings.HasPrefix(s, \"hskey-api-\") || len(s) >= len(\"hskey-api-\")+12\n}","tryCatchPattern":"if _, err := db.ParseAPIKeyPrefix(display); err != nil {\n    if errors.Is(err, db.ErrAPIKeyFailedToParse) {\n        // reject the input, prompt the user to re-copy the key\n    }\n}","preventionTips":["Store and pass API keys whole - never substring or rebuild them.","Validate key length at the API boundary before passing to ParseAPIKeyPrefix.","Copy keys from 'headscale apikeys create' output directly; avoid re-typing."],"tags":["api-key","validation","parsing","user-input"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}