tailscale/tailscale · error
Could not accept WebSocket connection %v
Error message
Could not accept WebSocket connection %v
What it means
Server-side: nhooyr/websocket's Accept rejected the WebSocket upgrade request (malformed or missing Sec-WebSocket-Key/Version, wrong method, header violations). Note the underlying error is attached with %v, not %w, so errors.Is/As will not unwrap it. Compression is intentionally disabled because Noise traffic is incompressible and Safari's compression is broken.
Source
Thrown at control/controlhttp/controlhttpserver/controlhttpserver.go:138
return nc, nil
}
// acceptWebsocket upgrades a WebSocket connection (from a client that cannot
// speak HTTP) to a Tailscale control protocol base transport connection.
func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) {
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
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)View on GitHub (pinned to 0fd2f14deb)
Solutions
- Use a real WebSocket client library (coder/websocket, gorilla/websocket) instead of raw HTTP
- Ensure intermediaries forward the Sec-* headers unmodified
- Match the server's expectations: GET request, HTTP/1.1, valid Sec-WebSocket-Key
Example fix
// before
resp, _ := http.Get("wss://ctrl/ts2021") // no websocket handshake -> Accept fails
// after
ctx := context.Background()
c, _, err := websocket.Dial(ctx, "wss://ctrl/ts2021", &websocket.DialOptions{
Subprotocols: []string{"tailscale-control-protocol"},
CompressionMode: websocket.CompressionDisabled,
}) Defensive patterns
Strategy: try-catch
Validate before calling
// Client-side: verify you are speaking real WebSocket before hitting the server
if !strings.HasPrefix(url, "ws://") && !strings.HasPrefix(url, "wss://") {
return errors.New("control websocket endpoint requires a ws:// or wss:// URL")
} Try / catch
c, err := websocket.Accept(w, r, opts)
if err != nil {
// Note: underlying cause is %v-formatted; match on message, errors.Is will not work
log.Printf("bad websocket handshake from %s: %v", r.RemoteAddr, err)
return // request already rejected with an HTTP error
} Prevention
- Use a maintained websocket library; never hand-roll the handshake
- Ensure proxies forward all Sec-WebSocket-* headers
- Remember the cause is not wrapped (%v), so build monitoring on message prefixes
When it happens
Trigger: A plain HTTP request hitting the websocket path; proxies stripping WebSocket headers; clients not using a real WebSocket library; Origin patterns are '*' so origin is not the blocker — header validity is.
Common situations: Anti-virus/SSL inspectors breaking websocket handshakes, curl-based probes, hand-rolled clients, corporate proxies disabling websocket forwarding.
Related errors
- no tailscale handshake parameter in HTTP request
- no tailscale handshake header in HTTP request
- decoding base64 handshake header: %v
- flushing hijacked HTTP buffer: %w
- noise handshake failed: %w
AI-assisted analysis of tailscale/tailscale@0fd2f14deb (2026-08-18).
Data as JSON: /api/errors/33fdf96e0da87445.
Report an issue: GitHub.