{"record":{"id":"9585a4e826e30c17","repo":"juanfont/headscale","slug":"invalid-capability-version","errorCode":null,"errorMessage":"invalid capability version","messagePattern":"invalid capability version","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"warning","filePath":"hscontrol/handlers.go","lineNumber":114,"sourceCode":"\treturn HTTPError{Code: code, Msg: msg, Err: err}\n}\n\nvar errMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed, \"method not allowed\", nil)\n\nvar ErrRegisterMethodCLIDoesNotSupportExpire = errors.New(\n\t\"machines registered with CLI do not support expiry\",\n)\n\nfunc parseCapabilityVersion(req *http.Request) (tailcfg.CapabilityVersion, error) {\n\tclientCapabilityStr := req.URL.Query().Get(\"v\")\n\n\tif clientCapabilityStr == \"\" {\n\t\treturn 0, NewHTTPError(http.StatusBadRequest, \"capability version must be set\", nil)\n\t}\n\n\tclientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)\n\tif err != nil {\n\t\treturn 0, NewHTTPError(http.StatusBadRequest, \"invalid capability version\", fmt.Errorf(\"parsing capability version: %w\", err))\n\t}\n\n\treturn tailcfg.CapabilityVersion(clientCapabilityVersion), nil\n}\n\n// verifyBodyLimit caps the request body for /verify. The DERP verify\n// protocol payload ([tailcfg.DERPAdmitClientRequest]) is a few hundred\n// bytes; 4 KiB is generous and prevents an unauthenticated client from\n// OOMing the public router with arbitrarily large POSTs.\nconst verifyBodyLimit int64 = 4 * 1024\n\nfunc (h *Headscale) handleVerifyRequest(\n\treq *http.Request,\n\twriter io.Writer,\n) error {\n\tbody, err := io.ReadAll(req.Body)\n\tif err != nil {\n\t\treturn NewHTTPError(http.StatusRequestEntityTooLarge, \"request body too large\", fmt.Errorf(\"reading request body: %w\", err))","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/handlers.go#L96-L132","documentation":"Thrown by parseCapabilityVersion in hscontrol/handlers.go when the 'v' query parameter on a control-plane HTTP endpoint (e.g. /key or /ts2021) is present but cannot be parsed as an integer by strconv.Atoi. The capability version tells the server which tailcfg feature set the client speaks; a non-numeric value means the request did not come from a well-formed tailscaled client.","triggerScenarios":"A GET to /key?v=abc or /ts2021?v=1.2 (any non-integer 'v'), or a hand-crafted curl against the control port that includes a malformed version parameter.","commonSituations":"Manually testing the control endpoint with curl and forgetting that 'v' must be an integer; a proxy or middleware rewriting the query string; a non-Tailscale client scraping the server.","solutions":["If testing manually, use an integer version, e.g. curl 'http://server/key?v=94'","If seen from a real client, capture the full request URL and check for proxies/load balancers that mangle query strings","Verify the client is an actual tailscaled/ipn client and not a custom script sending garbage"],"exampleFix":"// before\ncurl http://headscale:8080/key?v=latest\n\n// after\ncurl http://headscale:8080/key?v=94","handlingStrategy":"validation","validationCode":"u, _ := url.Parse(rawQuery)\nv := u.Query().Get(\"v\")\nif n, err := strconv.Atoi(v); err != nil || v == \"\" {\n    return fmt.Errorf(\"capability version must be an integer, got %q\", v)\n} else if !isSupportedVersion(tailcfg.CapabilityVersion(n)) {\n    return fmt.Errorf(\"unsupported capability version %d\", n)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always construct control-plane URLs with an integer capability version taken from tailcfg.CurrentCapabilityVersion","When scripting against headscale, validate query parameters before sending"],"tags":["http","validation","capability-version","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}