Browser WebSocket (wss://) fails with "WebSocket opening handshake was canceled" in Chrome, while ordinary HTTPS requests to the same host succeed. nginx logs no access-log entry for the upgrade request at all, and the error log is silent, because the connection dies at the TLS layer before any HTTP request completes. Server-side WebSocket code is correct and returns proper HTTP responses (e.g. 401) when tested with a non-browser client such as curl or the ws Node library.

nginx · verified Sep 16, 2026

Fix: Root cause: the nginx server block terminating the WebSocket has `ssl_verify_client optional` (or `on`), so nginx sends a TLS CertificateRequest to every client on that vhost. Chrome cancels any WebSocket handshake that receives a certificate selection request when no certificate selection is already cached for that origin, because Chromium policy forbids showing UI for subresources — and a WebSocket is a subresource. Ordinary XHR/fetch to the same host is unaffected, which is what makes it look like a WebSocket-specific server bug. `ssl_verify_client` is server-scoped in nginx: the CertificateRequest is sent during the TLS handshake, before any path is known, so no `location` block can exempt the WebSocket endpoint. The fix is to split the vhost by audience — a browser-facing server_name with no `ssl_client_certificate` / `ssl_verify_client` directives at all, and a separate internal server_name with `ssl_verify_client on` for service-to-service mTLS callers. Important security note when splitting: if the application trusts `X-SSL-Client-Verify` / `X-SSL-Client-Subject` headers set by nginx, the browser-facing vhost must explicitly blank those headers (`proxy_set_header X-SSL-Client-Verify "";`). Otherwise removing the client-cert directives leaves those headers attacker-controlled, since nginx no longer overwrites them. Diagnostic that identifies this quickly: `openssl s_client -connect HOST:443 -servername HOST </dev/null | grep -A3 "Acceptable client certificate CA names"`. If the CA list prints, the vhost is requesting client certificates and Chrome WebSockets to it will be canceled.

nginxwebsocketmtlsssl_verify_clientchromechromiumclient-certificatetls

References