Request and response header
< Upgrade: websocketUpgrade Header
Learn how the Upgrade header requests protocol upgrades to WebSocket, HTTP/2, or other protocols on the same TCP connection. Understand upgrade negotiation.
- Direction
- Request and response
- Category
- Connection management
- Spec
- RFC 9110 §7.8
- JS can set it
- No: forbidden header name
- Hop-by-hop
- Yes: not forwarded by proxies
On this page
TL;DR:
Upgraderequests an HTTP/1.1 protocol switch on the same connection. WebSocket usesUpgrade: websocketplusConnection: Upgradeand a validated101response. The oldh2cupgrade to HTTP/2 is obsolete in RFC 9113.
A client offers one or more protocols; the server may accept with 101 Switching Protocols or continue responding in HTTP. Upgrade applies only to the immediate hop. RFC 9110 §7.8 requires its sender to name it in Connection.
How Upgrade Works
This illustrative WebSocket exchange uses the test key and matching accept value from RFC 6455 §1.3:
GET /chat HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
After the blank line ending the response headers, traffic uses WebSocket frames, not an HTTP response body. An HTTP 200 response does not establish this WebSocket connection.
Syntax
Upgrade: protocol-name[/protocol-version], another-protocol
Protocol names and versions come from the chosen protocol specification; arbitrary strings do not make a server support a new protocol.
WebSocket Upgrade
The server must validate the opening handshake, including a key that decodes to 16 bytes and version 13. Browser requests include Origin, which a server can check against permitted page origins. If a server selects a subprotocol, it must be one the client offered. See RFC 6455 §4.2.
HTTP/2 Upgrade
RFC 7540 described Upgrade: h2c and HTTP2-Settings. RFC 9113 §3.1 removes that mechanism; §§11.1–11.2 mark the header and token obsolete. Current HTTP/2 uses TLS ALPN or prior knowledge for cleartext connections.
Server Implementation
For nginx proxying to an HTTP/1.1 WebSocket server at 127.0.0.1:3000, place this in an existing server block:
location /chat {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
nginx does not forward hop-by-hop headers automatically. Its WebSocket proxying documentation explains why they must be supplied explicitly. By default, nginx closes this proxied connection if the upstream sends no data for 60 seconds; proxy_read_timeout changes that wait, and server ping frames can keep traffic flowing.
Node.js with ws
For a working local echo endpoint, install the protocol library with pnpm add ws. Save as echo.mjs and run node echo.mjs:
import http from 'node:http'
import { WebSocketServer } from 'ws'
const server = http.createServer((req, res) => {
res.writeHead(404)
res.end()
})
const wss = new WebSocketServer({ noServer: true })
server.on('upgrade', (req, socket, head) => {
const path = new URL(req.url, 'http://localhost').pathname
if (path !== '/chat') {
socket.end('HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n')
return
}
const origin = req.headers.origin
if (origin && origin !== 'http://localhost:3000') {
socket.end('HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n')
return
}
wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req))
})
wss.on('connection', (ws) => {
ws.on('error', console.error)
ws.on('message', (data, isBinary) => ws.send(data, { binary: isBinary }))
})
server.listen(3000, '127.0.0.1')
handleUpgrade() validates the handshake and retains bytes already read in head. The library also handles masking and frame parsing. The endpoint allows a missing Origin for non-browser diagnostic clients; that policy is not authentication. For a private endpoint, verify the session before calling handleUpgrade(). See ws’s upgrade and authentication examples.
Go with gorilla/websocket
In a Go module, install github.com/gorilla/websocket with go get github.com/gorilla/websocket. This alternate local server keeps the Upgrader’s default origin check:
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{}
func main() {
http.HandleFunc("/chat", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { return }
defer conn.Close()
for {
kind, data, err := conn.ReadMessage()
if err != nil { return }
if err := conn.WriteMessage(kind, data); err != nil { return }
}
})
log.Fatal(http.ListenAndServe("127.0.0.1:3000", nil))
}
When Origin is present, the default policy rejects an Origin host different from the request Host. If a frontend on another hostname needs access, define a narrow CheckOrigin policy for that hostname. Returning true for every request removes the check.
Python with aiohttp
Install aiohttp in your Python environment and save this alternative echo server as echo.py. It handles both text and binary messages:
from aiohttp import web, WSMsgType
async def chat(request):
origin = request.headers.get('Origin')
if origin and origin != 'http://localhost:3000':
raise web.HTTPForbidden()
socket = web.WebSocketResponse()
await socket.prepare(request)
async for message in socket:
if message.type == WSMsgType.TEXT:
await socket.send_str(message.data)
elif message.type == WSMsgType.BINARY:
await socket.send_bytes(message.data)
elif message.type == WSMsgType.ERROR:
print(socket.exception())
return socket
app = web.Application()
app.router.add_get('/chat', chat)
web.run_app(app, host='127.0.0.1', port=3000)
Run python echo.py instead of the Node or Go server. prepare() performs the opening handshake; after it succeeds, use WebSocket send methods rather than writing an HTTP response body. The aiohttp server guide documents this lifecycle. WSMsgType belongs to aiohttp, not aiohttp.web.
Caddy reverse proxy
Caddy handles WebSocket upgrades through its ordinary reverse proxy. For a local test, save this as Caddyfile:
:8080 {
reverse_proxy 127.0.0.1:3000
}
Run caddy run --config Caddyfile and connect the client to ws://localhost:8080/chat. The backend still receives /chat; no path rewrite is configured. Unlike the nginx example, this does not require manually supplying Upgrade and Connection. Caddy’s reverse proxy documentation describes the switch to a bidirectional tunnel. A browser request still carries its page Origin, so account for that origin in the backend’s allowlist.
Client Implementation
In a browser, use the WebSocket API; it generates the handshake:
const socket = new WebSocket('wss://example.com/chat')
socket.addEventListener('open', () => socket.send('hello'))
socket.addEventListener('message', (event) => console.log(event.data))
The constructor accepts a URL and optional subprotocols, not arbitrary request headers. See the WebSockets Standard.
For the local echo server, a Node client can send headers that the browser constructor cannot. Save this as client.mjs after installing ws:
import WebSocket from 'ws'
const socket = new WebSocket('ws://127.0.0.1:3000/chat')
socket.on('error', console.error)
socket.on('open', () => socket.send('diagnostic echo'))
socket.on('message', (data) => {
console.log(data.toString())
socket.close()
})
A Python client provides another independent check. Install websockets in your Python environment, then run:
import asyncio
from websockets.asyncio.client import connect
async def main():
async with connect('ws://127.0.0.1:3000/chat') as socket:
await socket.send('diagnostic echo')
print(await socket.recv())
asyncio.run(main())
The async context manager closes the connection on exit. If these clients work but the browser does not, compare its Origin and offered subprotocols with server policy. An Origin check can distinguish permitted browser pages; non-browser clients can supply that field themselves.
Security Considerations
An accept hash confirms handshake processing, not user authentication. Authorize access before accepting the connection, and use wss:// for TLS. Do not copy a server example that unconditionally accepts every Origin if access depends on browser cookies.
For ws, ping/pong can detect a connection that disappeared without a close frame. Add this inside the connection handler; the 30-second interval is a chosen application setting:
let alive = true
ws.on('pong', () => { alive = true })
const heartbeat = setInterval(() => {
if (!alive) {
ws.terminate()
return
}
alive = false
ws.ping()
}, 30000)
ws.on('close', () => clearInterval(heartbeat))
The ws heartbeat example uses this pattern. Align the interval with the proxy’s no-data timeout. A client ping alone may not satisfy a timer that specifically waits for data from the upstream server; make sure server responses or server pings traverse that leg.
Getting Upgrade right
Parse Connection as comma-separated tokens rather than checking whether a string contains the substring upgrade. notupgrade is not the Upgrade token. For a Node upgrade handler, this checks the token list:
const hasUpgradeToken = (req.headers.connection ?? '')
.split(',')
.some((token) => token.trim().toLowerCase() === 'upgrade')
Reject a missing token before switching protocols. This checks one field only; let the WebSocket library validate the remaining handshake, including the key and version. A substring check can accept notupgrade and conceal a malformed request.
Common Issues
Upgrade: WebSocket is not inherently wrong: RFC 6455 matches this value case-insensitively. Missing companion headers, a stripped key, or a normal HTTP response are reasons to inspect both sides of the proxy.
To inspect a handshake against your own local endpoint:
curl --http1.1 -i -N --max-time 5 \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
http://localhost:3000/chat
This uses a fixed diagnostic key, not a production client’s random key. The bounded command inspects the response; it is not an interactive WebSocket application.
HTTP/2 Considerations
Do not forward Connection or Upgrade into HTTP/2 or HTTP/3 messages. WebSocket over HTTP/2 uses extended CONNECT, defined separately in RFC 8441, rather than this HTTP/1.1 exchange.
Related Headers
Frequently asked questions
What is the Upgrade header?
Upgrade requests switching to a different protocol on the same connection. Most commonly used to upgrade HTTP to WebSocket. Server responds with 101 if it agrees.
How does WebSocket upgrade work?
For HTTP/1.1, the client sends Upgrade: websocket, Connection: Upgrade, and the WebSocket handshake fields. The server must return a valid 101 response with a matching Sec-WebSocket-Accept.
Can Upgrade be used for HTTP/2?
The historical h2c upgrade mechanism is obsolete in RFC 9113. Current HTTP/2 starts through TLS ALPN or prior knowledge on cleartext connections.
What happens if server ignores Upgrade?
Server can ignore Upgrade and respond normally with HTTP. The client should handle both upgraded and non-upgraded responses gracefully.
Sources
Related
Via Header
Learn how the Via header tracks the path of HTTP requests through proxies and gateways. Debug routing issues and understand your network infrastructure.
WebSockets over HTTP: Handshakes, Proxies and Auth
Trace a WebSocket HTTP handshake, verify Sec-WebSocket-Accept, configure nginx, secure browser authentication, and debug idle disconnects and close codes.
Sec-WebSocket-Accept
Learn how the Sec-WebSocket-Accept header indicates server acceptance of a WebSocket connection upgrade. Understand the handshake process and key validation.
Sec-WebSocket-Key
Learn how the Sec-WebSocket-Key header provides a random key for WebSocket handshake validation. Understand the upgrade process and security implications.