gorilla/websocket · error
websocket: bad handshake
Error message
websocket: bad handshake
What it means
ErrBadHandshake is the sentinel error returned by Dialer.Dial/DialContext when the server's response to the WebSocket opening handshake is invalid — typically a non-101 HTTP status. The actual HTTP response is returned alongside so callers can inspect it. It wraps any deviation from a valid server handshake response.
Source
Thrown at client.go:24
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"strings"
"time"
)
// ErrBadHandshake is returned when the server response to opening handshake is
// invalid.
var ErrBadHandshake = errors.New("websocket: bad handshake")
var errInvalidCompression = errors.New("websocket: invalid compression negotiation")
// NewClient creates a new client connection using the given net connection.
// The URL u specifies the host and request URI. Use requestHeader to specify
// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies
// (Cookie). Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
//
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication,
// etc.
//
// Deprecated: Use Dialer instead.
func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) {
d := Dialer{
ReadBufferSize: readBufSize,
WriteBufferSize: writeBufSize,View on GitHub (pinned to e064f32e36)
Solutions
- Check the returned *http.Response (its StatusCode and Body) to see why the handshake failed and fix the server or URL
- Verify the URL uses ws:// or wss:// and the path actually performs the WebSocket upgrade server-side
- Ensure any proxy/load balancer in front supports and forwards Upgrade/Connection headers
- If a non-101 status is expected (e.g. 401), handle auth before dialing (header, cookie, or ticket)
Example fix
// before
conn, resp, err := dialer.Dial("wss://api.example.com/socket", nil)
if err != nil { log.Fatal(err) }
// after
conn, resp, err := dialer.Dial("wss://api.example.com/socket", nil)
if errors.Is(err, websocket.ErrBadHandshake) {
log.Printf("handshake failed: status=%d body=%s", resp.StatusCode, readBody(resp.Body))
return
}
if err != nil { log.Fatal(err) } Defensive patterns
Strategy: type-guard
Validate before calling
u, err := url.Parse(wsURL)
if err != nil || (u.Scheme != "ws" && u.Scheme != "wss") {
return fmt.Errorf("invalid websocket url: %q", wsURL)
} Type guard
func isBadHandshake(err error) bool {
return errors.Is(err, websocket.ErrBadHandshake)
} Try / catch
conn, resp, err := dialer.DialContext(ctx, wsURL, nil)
if err != nil {
if errors.Is(err, websocket.ErrBadHandshake) {
// inspect resp.StatusCode / body to decide whether to retry or abort
return nil, fmt.Errorf("handshake rejected: status=%d", resp.StatusCode)
}
return nil, err // transport-level error: safe to retry with backoff
} Prevention
- Always check resp.StatusCode when ErrBadHandshake is returned
- Confirm the server path actually upgrades connections before dialing in production
- Test through the full proxy chain (nginx, ALB) since proxies often break Upgrade headers
- Distinguish handshake errors (don't retry) from network errors (retry with backoff)
When it happens
Trigger: Calling Dialer.Dial/DialContext against a server that does not complete the WebSocket upgrade: the endpoint returns 200/404/403/500 instead of 101 Switching Protocols, or the 101 response lacks required headers (Upgrade: websocket, Connection: Upgrade, valid Sec-WebSocket-Accept).
Common situations: Pointing the dialer at a plain HTTP REST endpoint, a reverse proxy or API gateway that strips Upgrade headers, missing auth leading to a 401/403 response page, misconfigured TLS terminating proxy, or server not supporting WebSockets on that path.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- websocket: invalid compression negotiation
- f[1] (proxy response status text)
- http.StatusText(status)
- malformed ws or wss URL
- websocket: duplicate header not allowed:
AI-assisted analysis of gorilla/websocket@e064f32e36 (2026-08-31).
Data as JSON: /api/errors/bd74d8445f57d99e.
Report an issue: GitHub.