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.URLView on GitHub (pinned to e064f32e36)
Solutions
- Remove the protected header from requestHeader and configure it via the dialer instead (e.g. d.Subprotocols for Sec-WebSocket-Protocol)
- Set Sec-WebSocket-Protocol ONLY through Dialer.Subprotocols when Subprotocols is used
- If you need extensions/keys, let the library generate them — do not set Sec-WebSocket-Extensions, Key, Version, Upgrade, or Connection manually
- 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
- Set subprotocols only via Dialer.Subprotocols, never both ways
- Audit header maps built for net/http before reusing them for websocket dials
- Let the library own Upgrade, Connection, Key, Version, Extensions headers
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
- websocket: protocol %q was given but is not supported;sharin
- websocket: bad handshake
- websocket: invalid compression negotiation
- malformed ws or wss URL
- websocket: internal error, unexpected bytes at end of flate
AI-assisted analysis of gorilla/websocket@e064f32e36 (2026-08-31).
Data as JSON: /api/errors/e9af304b891fae8a.
Report an issue: GitHub.