gorilla/websocket · error

websocket: duplicate header not allowed:

Error message

websocket: duplicate header not allowed: 

What it means

DialContext rejects user-supplied request headers that would conflict with the headers the library must set itself for the WebSocket handshake: Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Extensions, and Sec-WebSocket-Protocol (when Subprotocols is configured on the dialer). Supplying any of these as a duplicate is rejected with this error naming the offending header key.

Source

Thrown at client.go:245

	req.Header["Connection"] = []string{"Upgrade"}
	req.Header["Sec-WebSocket-Key"] = []string{challengeKey}
	req.Header["Sec-WebSocket-Version"] = []string{"13"}
	if len(d.Subprotocols) > 0 {
		req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")}
	}
	for k, vs := range requestHeader {
		switch {
		case k == "Host":
			if len(vs) > 0 {
				req.Host = vs[0]
			}
		case k == "Upgrade" ||
			k == "Connection" ||
			k == "Sec-Websocket-Key" ||
			k == "Sec-Websocket-Version" ||
			k == "Sec-Websocket-Extensions" ||
			(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
			return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
		case k == "Sec-Websocket-Protocol":
			req.Header["Sec-WebSocket-Protocol"] = vs
		default:
			req.Header[k] = vs
		}
	}

	if d.EnableCompression {
		req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"}
	}

	if d.HandshakeTimeout != 0 {
		var cancel func()
		ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout)
		defer cancel()
	}

	var proxyURL *url.URL

View on GitHub (pinned to e064f32e36)

Solutions

  1. Remove the protected header from requestHeader and configure it via the dialer instead (e.g. d.Subprotocols for Sec-WebSocket-Protocol)
  2. Set Sec-WebSocket-Protocol ONLY through Dialer.Subprotocols when Subprotocols is used
  3. If you need extensions/keys, let the library generate them — do not set Sec-WebSocket-Extensions, Key, Version, Upgrade, or Connection manually
  4. Keep application-specific headers (Authorization, Cookie, Origin) in requestHeader — those are allowed

Example fix

// before
dialer := websocket.Dialer{Subprotocols: []string{"chat"}}
header := http.Header{}
header.Set("Sec-WebSocket-Protocol", "chat")
conn, _, err := dialer.Dial(url, header)
// after
dialer := websocket.Dialer{Subprotocols: []string{"chat"}}
conn, _, err := dialer.Dial(url, nil)
Defensive patterns

Strategy: validation

Validate before calling

var forbidden = map[string]bool{
    "Upgrade": true, "Connection": true,
    "Sec-Websocket-Key": true, "Sec-Websocket-Version": true,
    "Sec-Websocket-Extensions": true,
}
for k := range header {
    if forbidden[http.CanonicalHeaderKey(k)] || k == "Sec-Websocket-Protocol" && len(dialer.Subprotocols) > 0 {
        return fmt.Errorf("header %q is managed by the websocket dialer", k)
    }
}

Try / catch

conn, resp, err := dialer.DialContext(ctx, url, header)
if err != nil && strings.HasPrefix(err.Error(), "websocket: duplicate header not allowed") {
    return fmt.Errorf("remove the protected header from requestHeader: %w", err)
}

Prevention

When it happens

Trigger: Passing one of the protected headers in requestHeader to Dial/DialContext — e.g. requestHeader.Set("Sec-WebSocket-Protocol", "chat") while dialer.Subprotocols is non-empty, or manually setting "Connection: Upgrade" or "Sec-WebSocket-Version: 13".

Common situations: Porting code from raw net/http clients where those headers were set manually, copying browser examples that set Upgrade/Connection headers, and specifying Sec-WebSocket-Protocol both via dialer.Subprotocols and via request header.

Related errors


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