Fix Daphne ASGI compatibility and document WS handshake cookie limitation

- AsgiContext and AsgiWebSocket now default missing  scope key
  to 'http' / 'ws' respectively (Daphne omits it for websocket scopes)
- Add regression test for websocket scope without scheme
- Update example/session.py WS handler to read the session; cookie must
  be set via HTTP first because common ASGI servers ignore the headers
  field on websocket.accept
- Update README with the same caveat about Granian/Daphne/curl
This commit is contained in:
2026-07-23 22:11:06 +08:00
parent e4e00762bb
commit ca023580e4
4 changed files with 39 additions and 12 deletions
+6 -2
View File
@@ -35,7 +35,11 @@ async def echo(ws: WebSocket) -> None:
@app.websocket('/ws/visits')
async def ws_visits(ws: WebSocket) -> None:
visits = ws.session.get('visits', 0) + 1
ws.session['visits'] = visits
# WebSocket handlers can read the existing session. Most ASGI servers
# (including Granian and Daphne) do not forward the `headers` field of the
# `websocket.accept` message into the HTTP 101 response, so a new session
# cookie cannot be set during the handshake. Use the HTTP `/` endpoint to
# set or refresh the session cookie before connecting here.
await ws.accept()
visits = ws.session.get('visits', 0)
await ws.send_text(f'visits: {visits}')
+2 -2
View File
@@ -83,7 +83,7 @@ class AsgiContext(HttpContext):
self.path = scope['path']
self.query_string = scope['query_string'].decode()
self.method = HttpMethod(scope['method'])
self.scheme = scope['scheme']
self.scheme = scope.get('scheme', 'http')
self.client = scope['client']
self.server = scope['server']
self.headers = decode_headers(scope['headers'])
@@ -168,7 +168,7 @@ class AsgiWebSocket(WebSocket):
self._send = send
self.path = scope['path']
self.query_string = scope['query_string'].decode()
self.scheme = scope['scheme']
self.scheme = scope.get('scheme', 'ws')
self.client = scope['client']
self.server = scope['server']
self.headers = decode_headers(scope['headers'])
@@ -131,3 +131,23 @@ class WebSocketTest(unittest.TestCase):
await ws.accept()
self.assertEqual(1, len(sent_messages))
self.assertEqual({'type': 'websocket.accept'}, sent_messages[0])
@async_test
async def test_websocket_scope_without_scheme(self):
# Daphne omits the optional `scheme` key from websocket scopes.
async def send(message):
pass
async def receive():
return {'type': 'websocket.connect'}
scope = {
'type': 'websocket',
'path': '/echo',
'query_string': b'',
'client': ('127.0.0.1', 12345),
'server': ('127.0.0.1', 80),
'headers': [],
}
ws = AsgiWebSocket(scope, receive, send)
self.assertEqual('ws', ws.scheme)
+11 -8
View File
@@ -34,19 +34,22 @@ The same session is available in websocket handlers as `ws.session`:
```python
@app.websocket('/ws/visits')
async def ws_visits(ws: WebSocket):
visits = ws.session.get('visits', 0) + 1
ws.session['visits'] = visits
await ws.accept()
visits = ws.session.get('visits', 0)
await ws.send_text(f'visits: {visits}')
```
The session is loaded from the cookie when the connection is opened and
persisted when the connection closes, if it was modified. The session cookie
can only be set or refreshed on the handshake response, so mutate the session
*before* calling `ws.accept()` if you want the cookie delivered with the
handshake. Handshake cookies require ASGI spec version 2.1+; RSGI websocket
handshakes cannot carry response headers, so on RSGI the session is loaded and
persisted but the cookie is only set or refreshed by HTTP responses.
persisted when the connection closes, if it was modified.
**Important:** although the ASGI spec allows custom headers on the WebSocket
handshake response (`websocket.accept` headers, spec 2.1+), most ASGI servers
in practice — including **Granian** and **Daphne** — do not forward them into
the HTTP `101` response. RSGI websocket handshakes cannot carry response headers
at all. Therefore, in real deployments a session cookie can only be set or
refreshed by an HTTP response. Use an HTTP endpoint to establish or update the
session before opening the WebSocket, and read the existing session in the
WebSocket handler.
## Session expiry