{"record":{"id":"bda0527ecb1e25f0","repo":"juanfont/headscale","slug":"w-secret-length-mismatch-expected-d-chars-got","errorCode":null,"errorMessage":"%w: secret length mismatch, expected %d chars, got %d","messagePattern":"%w: secret length mismatch, expected (.+?) chars, got (.+?)","errorType":"validation","errorClass":"parseErr (ErrPreAuthKeyFailedToParse|ErrOAuthClientFailedToParse|ErrAccessTokenFailedToParse|ErrAPIKeyFailedToParse)","httpStatus":401,"severity":"error","filePath":"hscontrol/db/preauth_keys.go","lineNumber":270,"sourceCode":"\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}\n\n\t// Validate prefix contains only base64 URL-safe characters\n\tif !isValidBase64URLSafe(prefix) {\n\t\treturn \"\", \"\", fmt.Errorf(\n\t\t\t\"%w: prefix contains invalid characters (expected base64 URL-safe: A-Za-z0-9_-)\",\n\t\t\tparseErr,\n\t\t)\n\t}\n\n\t// Validate secret contains only base64 URL-safe characters\n\tif !isValidBase64URLSafe(secret) {\n\t\treturn \"\", \"\", fmt.Errorf(","sourceCodeStart":252,"sourceCodeEnd":288,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/preauth_keys.go#L252-L288","documentation":"parsePrefixedKey split the string correctly at the separator, but the remaining secret is not exactly secretLen (64) characters. Since the total length minimum was already checked, this specifically fires when the string is longer than expected — extra trailing characters after a valid secret.","triggerScenarios":"A key with trailing garbage: appended newline handling already done but extra characters pasted (e.g. two keys concatenated, a trailing quote or comma from JSON/YAML), or shell quoting that glued extra text onto the secret.","commonSituations":"Copy/paste grabbing the next line in a config file, JSON values quoted incorrectly, or scripts concatenating key + comment.","solutions":["Re-copy the key ensuring nothing follows the 64-char secret.","In config files, quote the whole key exactly once and keep it on one line.","Add a pre-check on exact length (77 chars after the scheme prefix) before using the key."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"func exactKeyLength(rest string) bool {\n    return len(rest) == 12+1+64 // fixed-layout format\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Check for characters accidentally appended (quotes, commas, second keys) when embedding in JSON/YAML.","Trim whitespace once, then enforce exact length.","Prefer dedicated secret fields over free-text config blocks for keys."],"tags":["preauth-key","validation","key-format"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}