{"record":{"id":"7b09aa070864811b","repo":"juanfont/headscale","slug":"failed-to-parse-apikey-invalid-display-prefix-for","errorCode":null,"errorMessage":"failed to parse ApiKey: invalid display prefix format","messagePattern":"failed to parse ApiKey: invalid display prefix format","errorType":"validation","errorClass":"ErrAPIKeyFailedToParse","httpStatus":500,"severity":"warning","filePath":"hscontrol/db/api_key.go","lineNumber":170,"sourceCode":"\t\tWhere(\"id = ?\", keyID).\n\t\tUpdate(\"user_id\", uint(userID)).Error\n}\n\n// ParseAPIKeyPrefix extracts the database prefix from a display prefix.\n// 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","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/api_key.go#L152-L188","documentation":"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.","triggerScenarios":"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).","commonSituations":"Forked/custom headscale builds where the key prefix constant was modified inconsistently; never occurs in stock builds.","solutions":["If running a fork: verify apiKeyPrefix is a single constant used in both HasPrefix and Cut - they cannot mismatch in stock code.","Treat hitting this error as a code regression: file a bug rather than working around it.","Use the standard 12-character bare prefix or full 'hskey-api-...' display form to bypass this branch entirely."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"prefix, err := db.ParseAPIKeyPrefix(display)\nif err != nil {\n    return fmt.Errorf(\"parsing API key prefix: %w\", err)\n}","preventionTips":["In forks, keep apiKeyPrefix as a single shared constant for both HasPrefix and Cut.","Treat this specific variant as a code bug - report it upstream rather than catching it in product code."],"tags":["api-key","validation","dead-code","parsing"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}