Originally published on Code Beneath.
The WebSocket protocol allows client and server to agree on a subprotocol before data exchange begins. This negotiation happens via the Sec-WebSocket-Protocol header during the initial HTTP upgrade. Many developers ignore subprotocols entirely, assuming a single protocol is sufficient, then hit interoperability walls when a client expects a specific format (like JSON-RPC) and the server sends raw bytes. This article shows you exactly how websocket sec-websocket-protocol subprotocol negotiation example works, with code that proposes subprotocols, validates them, detects mismatches, and implements fallback strategies. Understanding this mechanism prevents silent protocol violations and broken integrations.
What Subprotocols Are and Why They Matter
A WebSocket subprotocol is an application-level protocol name that signals how the endpoint will interpret messages. Common examples: "chat", "json.rpc", "mqtt", "wamp". It is not part of the core WebSocket spec; rather, it is a convention that lets two endpoints agree on message semantics before they exchange a single frame.
Without subprotocol negotiation, a client might send `{"method": "ping"}` expecting JSON-RPC parsing, while the server treats it as a plain string. Or a device might send MQTT packets and the server interprets them as raw bytes. Subprotocols prevent this by making the agreement explicit and verifiable.
The negotiation is simple: the client lists acceptable subprotocols in the Sec-WebSocket-Protocol header (comma-separated). The server picks exactly one, returns it in the response header, or returns nothing if it accepts any or rejects all. If the server picks a subprotocol the client did not propose, the connection fails. This constraint ensures both sides agree before handshake completes.
The HTTP Handshake: Client Proposal and Server Selection
During the WebSocket upgrade, the client sends:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 Sec-WebSocket-Protocol: chat, superchat The client proposes "chat" and "superchat" in order of preference. The server chooses one or rejects negotiation:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat If the server does not send Sec-WebSocket-Protocol, the connection is established with no subprotocol in effect. If the server sends a subprotocol the client did not list, a conforming client closes the connection. If the server wants to reject all proposals, it should not send the header and let the application layer handle it, or send a 400 Bad Request before upgrade.
Go Implementation: Client Proposing and Server Selecting
Here is a complete Go example showing the client proposing subprotocols and the server selecting one:
package main import ( "fmt" "log" "net/http" "strings" "github.com/gorilla/websocket" ) // Server accepts connections and selects subprotocols. func runServer() { upgrader := websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { // Client proposes subprotocols in the request. clientProposals := r.Header.Get("Sec-WebSocket-Protocol") fmt.Printf("Client proposed: %s\n", clientProposals) // Parse proposals (comma-separated, may have spaces). var selected string if clientProposals != "" { proposals := strings.Split(clientProposals, ",") for i := range proposals { proposals[i] = strings.TrimSpace(proposals[i]) } // Server policy: prefer "json.rpc" if available, else "chat". for _, p := range proposals { if p == "json.rpc" { selected = "json.rpc" break } if p == "chat" { selected = "chat" } } } // If no acceptable subprotocol, reject the upgrade. if selected == "" && clientProposals != "" { http.Error(w, "No acceptable subprotocol", http.StatusBadRequest) return } // Upgrade, setting the selected subprotocol. // The Sec-WebSocket-Protocol response header is set via the Subprotocols field. upgrader.Subprotocols = []string{selected} conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println("Upgrade error:", err) return } defer conn.Close() fmt.Printf("Connection established with subprotocol: %s\n", conn.Subprotocol()) // Echo messages (subprotocol-aware, in production). for { _, msg, err := conn.ReadMessage() if err != nil { log.Println("Read error:", err) break } fmt.Printf("Received on %s: %s\n", conn.Subprotocol(), string(msg)) if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil { log.Println("Write error:", err) break } } }) log.Println("Server listening on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) } // Client connects, proposing subprotocols. func runClient() { url := "ws://localhost:8080/ws" header := http.Header{} header.Add("Sec-WebSocket-Protocol", "json.rpc, chat") conn, _, err := websocket.DefaultDialer.Dial(url, header) if err != nil { log.Fatal("Dial error:", err) } defer conn.Close() fmt.Printf("Client connected. Server selected subprotocol: %s\n", conn.Subprotocol()) // Send a message. if err := conn.WriteMessage(websocket.TextMessage, []byte("hello")); err != nil { log.Fatal("Write error:", err) } _, msg, err := conn.ReadMessage() if err != nil { log.Fatal("Read error:", err) } fmt.Printf("Client received: %s\n", string(msg)) } func main() { // Uncomment one: runServer() // runClient() } Run the server in one terminal, then uncomment `runClient()` and run it in another. The output shows the client proposing "json.rpc" and "chat", the server selecting "json.rpc", and both sides knowing they are using the same subprotocol. The key is the `Subprotocols` field on the upgrader: it tells gorilla/websocket which subprotocol to echo back in the response.
Python Implementation: Client and Server with Error Handling
Here is a Python example using websockets (async) that handles subprotocol negotiation and mismatch errors:
import asyncio import websockets from websockets.server import WebSocketServerProtocol from websockets.client import WebSocketClientProtocol # Server: select subprotocol. async def server_handler(websocket: WebSocketServerProtocol, path: str): # websockets library provides request_headers. client_proposals_header = websocket.request_headers.get("sec-websocket-protocol", "") print(f"Server: client proposed: {client_proposals_header}") if client_proposals_header: proposals = [p.strip() for p in client_proposals_header.split(",")] selected = None # Server preference: json.rpc first, then chat, then mqtt. for preferred in ["json.rpc", "chat", "mqtt"]: if preferred in proposals: selected = preferred break if selected: # Set response subprotocol header via websocket.subprotocol. websocket.subprotocol = selected print(f"Server: selected subprotocol: {selected}") else: # No acceptable subprotocol; reject. await websocket.close(code=1002, reason="No acceptable subprotocol") print(f"Server: no acceptable subprotocol, rejected.") return else: print("Server: no subprotocol proposed, proceeding without one.") try: async for message in websocket: subproto = websocket.subprotocol or "(none)" print(f"Server: received on {subproto}: {message}") await websocket.send(f"Echo: {message}") except websockets.exceptions.ConnectionClosed: print("Server: connection closed") async def run_server(): async with websockets.serve(server_handler, "localhost", 8080): print("Server listening on ws://localhost:8080") await asyncio.Future() # run forever # Client: propose subprotocols and detect mismatch. async def run_client(): uri = "ws://localhost:8080" subprotocols = ["json.rpc", "chat"] try: async with websockets.connect( uri, subprotocols=subprotocols, max_size=None, ping_interval=None ) as websocket: selected = websocket.subprotocol print(f"Client: connected. Server selected: {selected}") # Validate that server picked one we proposed. if selected and selected not in subprotocols: print(f"Client: ERROR - server selected {selected}, but we only proposed {subprotocols}") await websocket.close(code=1002, reason="Subprotocol mismatch") return # Send and receive. await websocket.send("hello") response = await websocket.recv() print(f"Client: received: {response}") except websockets.exceptions.WebSocketException as e: print(f"Client: WebSocket error: {e}") # Test: run server in background, then client. async def main(): server_task = asyncio.create_task(run_server()) await asyncio.sleep(1) # Let server start. await run_client() server_task.cancel() try: await server_task except asyncio.CancelledError: pass if __name__ == "__main__": asyncio.run(main()) The Python websockets library handles subprotocol negotiation through the `subprotocols` parameter on the client side and the `websocket.subprotocol` attribute on both sides. Note the validation on the client: after connecting, check that `websocket.subprotocol` is in the list you proposed. If the server picked something unexpected, close the connection immediately with code 1002 (protocol error).
Handling Subprotocol Mismatches and Fallback Strategies
There are several failure modes and ways to recover:
Mismatch on server return: The server sends a Sec-WebSocket-Protocol header with a value the client did not propose. A correct client closes immediately. In your code, validate after connecting: if `conn.Subprotocol()` is not empty and not in your proposed list, close the connection.
No agreement at all: The client proposes subprotocols, but the server does not return one and closes the connection. This usually means the server does not support any of the proposed subprotocols. Fallback: try again with a broader list, or proceed without a subprotocol if the application permits.
Client proposes nothing: If the client sends no Sec-WebSocket-Protocol header, the server can either proceed without a subprotocol or reject the connection. Always be explicit: if your application requires a subprotocol, the server should reject any request without proposals.
Here is a Go helper function that implements a retry loop with fallback:
package main import ( "fmt" "log" "net/http" "strings" "github.com/gorilla/websocket" ) // DialWithFallback attempts to connect with subprotocols, retrying with fewer options on failure. func DialWithFallback(url string, preferredSubprotocols []string) (*websocket.Conn, string, error) { dialer := websocket.Dialer{} // Try each combination, starting with all, then removing each one. combinations := [][]string{ preferredSubprotocols, } // Generate fallback combinations (remove each subprotocol). for i := 0; i < len(preferredSubprotocols); i++ { fallback := make([]string, 0, len(preferredSubprotocols)-1) for j, sp := range preferredSubprotocols { if i != j { fallback = append(fallback, sp) } } if len(fallback) > 0 { combinations = append(combinations, fallback) } } // Try with no subprotocol. combinations = append(combinations, []string{}) var lastErr error for _, subprocs := range combinations { fmt.Printf("Trying with subprotocols: %v\n", subprocs) header := http.Header{} if len(subprocs) > 0 { header.Add("Sec-WebSocket-Protocol", strings.Join(subprocs, ", ")) } conn, _, err := dialer.Dial(url, header) if err != nil { lastErr = err fmt.Printf("Attempt failed: %v\n", err) continue } selected := conn.Subprotocol() if selected != "" && !contains(subprocs, selected) { fmt.Printf("ERROR: server selected %s, but we proposed %v\n", selected, subprocs) conn.Close() lastErr = fmt.Errorf("subprotocol mismatch: got %s", selected) continue } fmt.Printf("Connected with subprotocol: %s\n", selected) return conn, selected, nil } return nil, "", lastErr } func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } func main() { // Example usage. conn, selected, err := DialWithFallback( "ws://localhost:8080/ws", []string{"json.rpc", "chat", "mqtt"}, ) if err != nil { log.Fatal("Connection failed:", err) } defer conn.Close() fmt.Printf("Final connection: subprotocol=%s\n", selected) } Common Mistakes
Ignoring the Sec-WebSocket-Protocol header: Many developers never set or check subprotocols, then are surprised when a client and server disagree on message format. Always explicitly state subprotocols if your application has protocol requirements.
Server echoing an unproposed subprotocol: If the server receives no proposals or picks one not in the client's list, the client will close the connection. Ensure server logic never invents a subprotocol; it must choose from the client's list or send nothing.
Not validating on the client after connect: Some libraries do validate for you, but not all. Always check that the selected subprotocol matches what you proposed. Do not assume the handshake succeeded correctly.
Case sensitivity: Subprotocol names are case-sensitive. "Chat" and "chat" are different. Be consistent and document your subprotocol names clearly.
Forgetting the comma-space in proposals: The header format is comma-separated, but spaces are allowed. Some parsers trim spaces, others do not. Always trim after splitting.
FAQ
Can I change subprotocol mid-connection?
No. Subprotocol negotiation happens only during the HTTP upgrade. Once the WebSocket is open, the subprotocol is fixed. If you need a different subprotocol, you must close and reconnect.
What if I do not care about subprotocols?
If your application has no protocol versioning or variant requirements, you can omit subprotocols entirely. Both client and server proceed with `subprotocol = null` (or empty string). This is valid, but document it clearly so no one later assumes a subprotocol was negotiated.
Can the server propose subprotocols?
No. Only the client proposes. The server responds by selecting one from the client's list or not responding with a subprotocol header. This asymmetry ensures clarity: the client always knows what it is asking for.
What close code should I use if subprotocol negotiation fails?
Use code 1002 (Protocol Error) if there is a mismatch after negotiation, or 1008 (Policy Violation) if the server rejects all proposals before upgrade completes. On the HTTP side, return 400 Bad Request or 426 Upgrade Required before the WebSocket upgrade.
How do I know if the server returned a subprotocol?
Check the Sec-WebSocket-Protocol response header or use your library's accessor (e.g., `conn.Subprotocol()` in gorilla/websocket or `websocket.subprotocol` in Python websockets). If it is empty or null, no subprotocol was selected.
Are subprotocols the same as message types?
No. Subprotocols define the high-level protocol (e.g., MQTT, JSON-RPC, WAMP). Message types are a concept within a protocol (e.g., a JSON-RPC message has a "method" field). A subprotocol is negotiated once; message types are determined by the content of each message.
For deeper context on WebSocket fundamentals, see our WebSocket protocol guide, which covers framing, masking, and close semantics. Understanding how subprotocol negotiation fits into the broader WebSocket ecosystem helps you design robust, interoperable applications.
