{"record":{"id":"ca35706764acfd8b","repo":"juanfont/headscale","slug":"w-expected-separator-at-position-d-got-c","errorCode":null,"errorMessage":"%w: expected separator '-' at position %d, got '%c'","messagePattern":"%w: expected separator '-' at position (.+?), got '%c'","errorType":"validation","errorClass":"parseErr (ErrPreAuthKeyFailedToParse|ErrOAuthClientFailedToParse|ErrAccessTokenFailedToParse|ErrAPIKeyFailedToParse)","httpStatus":401,"severity":"error","filePath":"hscontrol/db/preauth_keys.go","lineNumber":258,"sourceCode":"\t//nolint:unparam // kept explicit though every credential kind uses a 12-char prefix and 64-char secret today\n\tprefixLen, secretLen int,\n\tparseErr error,\n) (string, string, error) {\n\texpectedMinLength := prefixLen + 1 + secretLen\n\tif len(prefixAndSecret) < expectedMinLength {\n\t\treturn \"\", \"\", fmt.Errorf(\n\t\t\t\"%w: key too short, expected at least %d chars after prefix, got %d\",\n\t\t\tparseErr,\n\t\t\texpectedMinLength,\n\t\t\tlen(prefixAndSecret),\n\t\t)\n\t}\n\n\tprefix := prefixAndSecret[:prefixLen]\n\n\t// Validate separator at expected position\n\tif prefixAndSecret[prefixLen] != '-' {\n\t\treturn \"\", \"\", fmt.Errorf(\n\t\t\t\"%w: expected separator '-' at position %d, got '%c'\",\n\t\t\tparseErr,\n\t\t\tprefixLen,\n\t\t\tprefixAndSecret[prefixLen],\n\t\t)\n\t}\n\n\tsecret := prefixAndSecret[prefixLen+1:]\n\n\t// Validate secret length\n\tif len(secret) != secretLen {\n\t\treturn \"\", \"\", fmt.Errorf(\n\t\t\t\"%w: secret length mismatch, expected %d chars, got %d\",\n\t\t\tparseErr,\n\t\t\tsecretLen,\n\t\t\tlen(secret),\n\t\t)\n\t}","sourceCodeStart":240,"sourceCodeEnd":276,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/preauth_keys.go#L240-L276","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Regenerate the key and pass it through unmodified.","If assembling programmatically, use fmt.Sprintf(\"%s-%s\", prefix, secret) with a 12-char prefix and 64-char secret.","Check for accidental character insertion/deletion around the 13th character after 'hskey-<kind>-'."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"func hasSeparatorAtPosition(rest string, pos int) bool {\n    return len(rest) > pos && rest[pos] == '-'\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never hand-assemble keys; use the issued string verbatim.","When concatenating parts programmatically, unit-test against a known-good key.","Remember the secret may itself contain '-', so position (not search) defines the separator."],"tags":["preauth-key","validation","key-format"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}