WebSockets¶
h2corn implements WebSockets on both transports the ASGI ecosystem expects:
- Classic RFC 6455 WebSockets over HTTP/1.1 with the
Upgradehandshake. - RFC 8441 "Bootstrapping WebSockets with HTTP/2", using the extended
CONNECTmethod on a single HTTP/2 stream.
To the application, both look identical: a websocket ASGI scope, a receive callable, and a send callable. The transport is negotiated between client and server with no application code changes.
Why the HTTP/2 transport matters¶
On HTTP/2, each WebSocket is a stream on the shared connection rather than a hijacked socket. Two practical consequences:
- A client can multiplex many WebSockets and ordinary HTTP requests on one TCP connection, instead of opening a fresh socket per stream.
- The proxy → app hop stays on HTTP/2 the whole time — no
Upgrade/Connection: keep-aliveinteraction with HTTP/1.1.
When a reverse proxy translates the browser's Upgrade into RFC 8441 extended CONNECT, WebSocket traffic can ride the same h2c connection as the rest of the application. See Behind a reverse proxy for the HTTP/1.1 split route and the pure HTTP/2 requirements.
Limits and keep-alives¶
The relevant configuration knobs share a websocket_* prefix; full descriptions, defaults, and CLI flags live in the WebSocket options:
websocket_max_message_sizecaps a reassembled message, not one frame: fragments are accounted as they arrive, and a compressed message is measured after decompression. The default is 16 MiB; set it to0to remove the cap entirely.websocket_per_message_deflatecontrols whether the server accepts the permessage-deflate compression extension when a client offers it.websocket_ping_intervalandwebsocket_ping_timeoutkeep idle connections alive and detect dead peers. Setwebsocket_ping_intervalto0to turn keep-alive off entirely — that is the single off switch; no pings are sent, and the timeout is irrelevant while keep-alive is unset.
Echo application¶
from fastapi import FastAPI, WebSocket
from starlette.websockets import WebSocketDisconnect
app = FastAPI()
@app.websocket('/ws')
async def echo(ws: WebSocket):
await ws.accept()
try:
while True:
message = await ws.receive_text()
await ws.send_text(f'echo: {message}')
except WebSocketDisconnect:
pass
Once the peer is gone, send() raises OSError — see What send() raises. Starlette surfaces that as WebSocketDisconnect.
HTTP/2-only deployment¶
h2corn ws:app --no-http1
With --no-http1, the server only accepts the HTTP/2 WebSocket bootstrap — the RFC 8441 extended CONNECT. Browser WebSocket APIs and the reverse proxy need support for the upgrade translation. If the proxy does not provide it, route WebSocket upgrades over HTTP/1.1 and ordinary requests over h2c, and keep HTTP/1 enabled there. See Behind a reverse proxy before dropping HTTP/1.