gorilla/websocket · error · HandshakeError

http.StatusText(status)

Error message

http.StatusText(status)

What it means

returnError is the Upgrader's error path during a failed WebSocket handshake. It writes an HTTP error response (with Sec-Websocket-Version: 13 header) whose body is http.StatusText(status), and returns a HandshakeError carrying the same reason to the caller. The developer sees this when an incoming request fails upgrade validation — the reason string is typically an HTTP status text such as 'Method Not Allowed', 'Bad Request', or 'Unauthorized'.

Source

Thrown at server.go:82

	//
	// A CheckOrigin function should carefully validate the request origin to
	// prevent cross-site request forgery.
	CheckOrigin func(r *http.Request) bool

	// EnableCompression specify if the server should attempt to negotiate per
	// message compression (RFC 7692). Setting this value to true does not
	// guarantee that compression will be supported. Currently only "no context
	// takeover" modes are supported.
	EnableCompression bool
}

func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) {
	err := HandshakeError{reason}
	if u.Error != nil {
		u.Error(w, r, status, err)
	} else {
		w.Header().Set("Sec-Websocket-Version", "13")
		http.Error(w, http.StatusText(status), status)
	}
	return nil, err
}

// checkSameOrigin returns true if the origin is not set or is equal to the request host.
func checkSameOrigin(r *http.Request) bool {
	origin := r.Header["Origin"]
	if len(origin) == 0 {
		return true
	}
	u, err := url.Parse(origin[0])
	if err != nil {
		return false
	}
	return equalASCIIFold(u.Host, r.Host)
}

func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string {

View on GitHub (pinned to e064f32e36)

Solutions

  1. Read the HandshakeError reason / returned status to see which validation failed (method, headers, version, origin, subprotocol)
  2. Ensure the client performs a real WS upgrade (use a WebSocket client, not plain fetch/curl without headers: Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key)
  3. If behind nginx/HAProxy, forward upgrade headers: proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"
  4. Relax or fix CheckOrigin to accept your frontend's Origin, instead of relying on the default same-origin check
  5. Ensure the client requests version 13 and, if using subprotocols, that the server lists a matching one

Example fix

// before (default same-origin check rejects cross-origin client)
var upgrader = websocket.Upgrader{}
conn, err := upgrader.Upgrade(w, r, nil) // err: "websocket: request origin not allowed by Upgrader.CheckOrigin"
// after: accept known origins
var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        origin := r.Header.Get("Origin")
        return origin == "https://app.example.com" || origin == ""
    },
}
conn, err := upgrader.Upgrade(w, r, nil)
Defensive patterns

Strategy: try-catch

Validate before calling

func canUpgrade(r *http.Request) error {
    if r.Method != http.MethodGet {
        return fmt.Errorf("need GET, got %s", r.Method)
    }
    if !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
        return errors.New("missing/invalid Upgrade header")
    }
    if !tokenListContains(r.Header.Get("Connection"), "upgrade") {
        return errors.New("missing Connection: Upgrade")
    }
    if r.Header.Get("Sec-Websocket-Key") == "" {
        return errors.New("missing Sec-WebSocket-Key")
    }
    if r.Header.Get("Sec-Websocket-Version") != "13" {
        return errors.New("unsupported websocket version")
    }
    return nil
}

Type guard

func isHandshakeError(err error) bool {
    _, ok := err.(websocket.HandshakeError)
    return ok
}

Try / catch

conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
    if isHandshakeError(err) {
        // Upgrade already wrote the HTTP error response; just log and return
        log.Printf("handshake rejected from %s: %v", r.RemoteAddr, err)
        return
    }
    http.Error(w, "internal error", http.StatusInternalServerError)
    return
}

Prevention

When it happens

Trigger: Upgrade() rejects a request: non-GET method, missing/invalid Upgrade or Connection headers, missing Sec-WebSocket-Key, unsupported Sec-WebSocket-Version, CheckOrigin returning false, or Subprotocol mismatch — each maps to a status whose StatusText becomes the error reason.

Common situations: Hitting the WS endpoint with a plain browser GET or curl without Upgrade headers; misconfigured reverse proxy/load balancer stripping Upgrade and Connection headers; CheckOrigin rejecting a legitimate cross-origin frontend (different host during development); client sending Sec-WebSocket-Version other than 13; accessing via wrong scheme without TLS termination.

Related errors


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