tailscale/tailscale · warning
parse query parameters: %v
Error message
parse query parameters: %v
What it means
Server-side: r.ParseForm failed while extracting the handshake parameter from the WebSocket request's query string. The query component is malformed (invalid percent-escapes, stray characters), which net/url rejects. Essentially only hostile or badly broken clients produce this.
Source
Thrown at control/controlhttp/controlhttpserver/controlhttpserver.go:146
Subprotocols: []string{controlhttpcommon.UpgradeHeaderValue},
OriginPatterns: []string{"*"},
// Disable compression because we transmit Noise messages that are not
// compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)
}
if c.Subprotocol() != controlhttpcommon.UpgradeHeaderValue {
c.Close(websocket.StatusPolicyViolation, "client must speak the control subprotocol")
return nil, fmt.Errorf("Unexpected subprotocol %q", c.Subprotocol())
}
if err := r.ParseForm(); err != nil {
c.Close(websocket.StatusPolicyViolation, "Could not parse parameters")
return nil, fmt.Errorf("parse query parameters: %v", err)
}
initB64 := r.Form.Get(controlhttpcommon.HandshakeHeaderName)
if initB64 == "" {
c.Close(websocket.StatusPolicyViolation, "missing Tailscale handshake parameter")
return nil, errors.New("no tailscale handshake parameter in HTTP request")
}
init, err := base64.StdEncoding.DecodeString(initB64)
if err != nil {
c.Close(websocket.StatusPolicyViolation, "invalid tailscale handshake parameter")
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
}
// Do not bind the conn's lifetime to ctx: it's typically a request
// context that net/http cancels once the calling handler returns, and
// the conn may be served beyond that (tailscale/corp#46806). The
// handshake below is still bounded by any ctx deadline, which
// controlbase.Server applies to the conn directly.
conn := wsconn.NetConn(context.WithoutCancel(ctx), c, websocket.MessageBinary, r.RemoteAddr)View on GitHub (pinned to 0fd2f14deb)
Solutions
- Build query strings with url.Values.Encode() instead of string concatenation
- Log the raw query on failure to identify the offending client
- Treat as a bad request: the socket is already closed with 1008 by the server
Example fix
// before
u := fmt.Sprintf("wss://ctrl/ts2021?X-Tailscale-Handshake=%s&ts=%d", initB64, time.Now().Unix())
// after
q := url.Values{}
q.Set(controlhttpcommon.HandshakeHeaderName, initB64)
u := "wss://ctrl/ts2021?" + q.Encode() Defensive patterns
Strategy: try-catch
Validate before calling
// Client-side: build the query with url.Values so it always parses server-side
q := url.Values{controlhttpcommon.HandshakeHeaderName: []string{initB64}}
u := endpoint + "?" + q.Encode() Try / catch
if err := r.ParseForm(); err != nil {
// Socket already closed with 1008; just log and drop the request
log.Printf("malformed query from %s: %v", r.RemoteAddr, err)
return
} Prevention
- Never concatenate query strings by hand; use net/url builders
- Log raw queries on parse failure to identify hostile clients
When it happens
Trigger: Query strings containing invalid escapes like '%zz', control characters, or truncated URLs; fuzzers and scanners sending malformed URLs to the control endpoint.
Common situations: Hand-built URL strings without proper encoding, fuzz testing of the websocket endpoint, upstream bugs concatenating query params unsafely.
Related errors
- no next protocol in HTTP request
- no tailscale handshake parameter in HTTP request
- client requested unhandled next protocol %q
- decoding base64 handshake header: %v
- Could not accept WebSocket connection %v
AI-assisted analysis of tailscale/tailscale@0fd2f14deb (2026-08-18).
Data as JSON: /api/errors/8c6797115e987a33.
Report an issue: GitHub.