{"record":{"id":"33fdf96e0da87445","repo":"tailscale/tailscale","slug":"could-not-accept-websocket-connection-v","errorCode":null,"errorMessage":"Could not accept WebSocket connection %v","messagePattern":"Could not accept WebSocket connection (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"control/controlhttp/controlhttpserver/controlhttpserver.go","lineNumber":138,"sourceCode":"\n\treturn nc, nil\n}\n\n// acceptWebsocket upgrades a WebSocket connection (from a client that cannot\n// speak HTTP) to a Tailscale control protocol base transport connection.\nfunc acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) {\n\tc, err := websocket.Accept(w, r, &websocket.AcceptOptions{\n\t\tSubprotocols:   []string{controlhttpcommon.UpgradeHeaderValue},\n\t\tOriginPatterns: []string{\"*\"},\n\t\t// Disable compression because we transmit Noise messages that are not\n\t\t// compressible.\n\t\t// Additionally, Safari has a broken implementation of compression\n\t\t// (see https://github.com/nhooyr/websocket/issues/218) that makes\n\t\t// enabling it actively harmful.\n\t\tCompressionMode: websocket.CompressionDisabled,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Could not accept WebSocket connection %v\", err)\n\t}\n\tif c.Subprotocol() != controlhttpcommon.UpgradeHeaderValue {\n\t\tc.Close(websocket.StatusPolicyViolation, \"client must speak the control subprotocol\")\n\t\treturn nil, fmt.Errorf(\"Unexpected subprotocol %q\", c.Subprotocol())\n\t}\n\tif err := r.ParseForm(); err != nil {\n\t\tc.Close(websocket.StatusPolicyViolation, \"Could not parse parameters\")\n\t\treturn nil, fmt.Errorf(\"parse query parameters: %v\", err)\n\t}\n\tinitB64 := r.Form.Get(controlhttpcommon.HandshakeHeaderName)\n\tif initB64 == \"\" {\n\t\tc.Close(websocket.StatusPolicyViolation, \"missing Tailscale handshake parameter\")\n\t\treturn nil, errors.New(\"no tailscale handshake parameter in HTTP request\")\n\t}\n\tinit, err := base64.StdEncoding.DecodeString(initB64)\n\tif err != nil {\n\t\tc.Close(websocket.StatusPolicyViolation, \"invalid tailscale handshake parameter\")\n\t\treturn nil, fmt.Errorf(\"decoding base64 handshake parameter: %v\", err)","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/tailscale/tailscale/blob/0fd2f14deb478d09c95f38af2b0d35b492b63520/control/controlhttp/controlhttpserver/controlhttpserver.go#L120-L156","documentation":"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.","triggerScenarios":"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.","commonSituations":"Anti-virus/SSL inspectors breaking websocket handshakes, curl-based probes, hand-rolled clients, corporate proxies disabling websocket forwarding.","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"],"exampleFix":"// before\nresp, _ := http.Get(\"wss://ctrl/ts2021\") // no websocket handshake -> Accept fails\n\n// after\nctx := context.Background()\nc, _, err := websocket.Dial(ctx, \"wss://ctrl/ts2021\", &websocket.DialOptions{\n    Subprotocols: []string{\"tailscale-control-protocol\"},\n    CompressionMode: websocket.CompressionDisabled,\n})","handlingStrategy":"try-catch","validationCode":"// Client-side: verify you are speaking real WebSocket before hitting the server\nif !strings.HasPrefix(url, \"ws://\") && !strings.HasPrefix(url, \"wss://\") {\n    return errors.New(\"control websocket endpoint requires a ws:// or wss:// URL\")\n}","typeGuard":null,"tryCatchPattern":"c, err := websocket.Accept(w, r, opts)\nif err != nil {\n    // Note: underlying cause is %v-formatted; match on message, errors.Is will not work\n    log.Printf(\"bad websocket handshake from %s: %v\", r.RemoteAddr, err)\n    return // request already rejected with an HTTP error\n}","preventionTips":["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"],"tags":["server","websocket","handshake","http-headers"],"backgroundTag":"websocket-handshake-failed","analyzedSha":"0fd2f14deb478d09c95f38af2b0d35b492b63520","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}