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

  1. Check the returned *http.Response (its StatusCode and Body) to see why the handshake failed and fix the server or URL
  2. Verify the URL uses ws:// or wss:// and the path actually performs the WebSocket upgrade server-side
  3. Ensure any proxy/load balancer in front supports and forwards Upgrade/Connection headers
  4. 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

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

Related errors


AI-assisted analysis of gorilla/websocket@e064f32e36 (2026-08-31). Data as JSON: /api/errors/bd74d8445f57d99e. Report an issue: GitHub.