juanfont/headscale · warning · HTTPError
invalid capability version
Error message
invalid capability version
What it means
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.
Source
Thrown at hscontrol/handlers.go:114
return HTTPError{Code: code, Msg: msg, Err: err}
}
var errMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed, "method not allowed", nil)
var ErrRegisterMethodCLIDoesNotSupportExpire = errors.New(
"machines registered with CLI do not support expiry",
)
func parseCapabilityVersion(req *http.Request) (tailcfg.CapabilityVersion, error) {
clientCapabilityStr := req.URL.Query().Get("v")
if clientCapabilityStr == "" {
return 0, NewHTTPError(http.StatusBadRequest, "capability version must be set", nil)
}
clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
if err != nil {
return 0, NewHTTPError(http.StatusBadRequest, "invalid capability version", fmt.Errorf("parsing capability version: %w", err))
}
return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
}
// verifyBodyLimit caps the request body for /verify. The DERP verify
// protocol payload ([tailcfg.DERPAdmitClientRequest]) is a few hundred
// bytes; 4 KiB is generous and prevents an unauthenticated client from
// OOMing the public router with arbitrarily large POSTs.
const verifyBodyLimit int64 = 4 * 1024
func (h *Headscale) handleVerifyRequest(
req *http.Request,
writer io.Writer,
) error {
body, err := io.ReadAll(req.Body)
if err != nil {
return NewHTTPError(http.StatusRequestEntityTooLarge, "request body too large", fmt.Errorf("reading request body: %w", err))View on GitHub (pinned to 565fd254d0)
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
Example fix
// before curl http://headscale:8080/key?v=latest // after curl http://headscale:8080/key?v=94
Defensive patterns
Strategy: validation
Validate before calling
u, _ := url.Parse(rawQuery)
v := u.Query().Get("v")
if n, err := strconv.Atoi(v); err != nil || v == "" {
return fmt.Errorf("capability version must be an integer, got %q", v)
} else if !isSupportedVersion(tailcfg.CapabilityVersion(n)) {
return fmt.Errorf("unsupported capability version %d", n)
} Prevention
- Always construct control-plane URLs with an integer capability version taken from tailcfg.CurrentCapabilityVersion
- When scripting against headscale, validate query parameters before sending
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- HEADSCALE_CLI_API_KEY environment variable needs to be set
- failed to parse ApiKey
- failed to parse oauth client secret
- failed to parse oauth access token
- failed to parse auth-key
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/9585a4e826e30c17.
Report an issue: GitHub.