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:
+6
-2
@@ -35,7 +35,11 @@ async def echo(ws: WebSocket) -> None:
|
|||||||
|
|
||||||
@app.websocket('/ws/visits')
|
@app.websocket('/ws/visits')
|
||||||
async def ws_visits(ws: WebSocket) -> None:
|
async def ws_visits(ws: WebSocket) -> None:
|
||||||
visits = ws.session.get('visits', 0) + 1
|
# WebSocket handlers can read the existing session. Most ASGI servers
|
||||||
ws.session['visits'] = visits
|
# (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()
|
await ws.accept()
|
||||||
|
visits = ws.session.get('visits', 0)
|
||||||
await ws.send_text(f'visits: {visits}')
|
await ws.send_text(f'visits: {visits}')
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class AsgiContext(HttpContext):
|
|||||||
self.path = scope['path']
|
self.path = scope['path']
|
||||||
self.query_string = scope['query_string'].decode()
|
self.query_string = scope['query_string'].decode()
|
||||||
self.method = HttpMethod(scope['method'])
|
self.method = HttpMethod(scope['method'])
|
||||||
self.scheme = scope['scheme']
|
self.scheme = scope.get('scheme', 'http')
|
||||||
self.client = scope['client']
|
self.client = scope['client']
|
||||||
self.server = scope['server']
|
self.server = scope['server']
|
||||||
self.headers = decode_headers(scope['headers'])
|
self.headers = decode_headers(scope['headers'])
|
||||||
@@ -168,7 +168,7 @@ class AsgiWebSocket(WebSocket):
|
|||||||
self._send = send
|
self._send = send
|
||||||
self.path = scope['path']
|
self.path = scope['path']
|
||||||
self.query_string = scope['query_string'].decode()
|
self.query_string = scope['query_string'].decode()
|
||||||
self.scheme = scope['scheme']
|
self.scheme = scope.get('scheme', 'ws')
|
||||||
self.client = scope['client']
|
self.client = scope['client']
|
||||||
self.server = scope['server']
|
self.server = scope['server']
|
||||||
self.headers = decode_headers(scope['headers'])
|
self.headers = decode_headers(scope['headers'])
|
||||||
|
|||||||
@@ -131,3 +131,23 @@ class WebSocketTest(unittest.TestCase):
|
|||||||
await ws.accept()
|
await ws.accept()
|
||||||
self.assertEqual(1, len(sent_messages))
|
self.assertEqual(1, len(sent_messages))
|
||||||
self.assertEqual({'type': 'websocket.accept'}, sent_messages[0])
|
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)
|
||||||
|
|||||||
@@ -34,19 +34,22 @@ The same session is available in websocket handlers as `ws.session`:
|
|||||||
```python
|
```python
|
||||||
@app.websocket('/ws/visits')
|
@app.websocket('/ws/visits')
|
||||||
async def ws_visits(ws: WebSocket):
|
async def ws_visits(ws: WebSocket):
|
||||||
visits = ws.session.get('visits', 0) + 1
|
|
||||||
ws.session['visits'] = visits
|
|
||||||
await ws.accept()
|
await ws.accept()
|
||||||
|
visits = ws.session.get('visits', 0)
|
||||||
await ws.send_text(f'visits: {visits}')
|
await ws.send_text(f'visits: {visits}')
|
||||||
```
|
```
|
||||||
|
|
||||||
The session is loaded from the cookie when the connection is opened and
|
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
|
persisted when the connection closes, if it was modified.
|
||||||
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
|
**Important:** although the ASGI spec allows custom headers on the WebSocket
|
||||||
handshake. Handshake cookies require ASGI spec version 2.1+; RSGI websocket
|
handshake response (`websocket.accept` headers, spec 2.1+), most ASGI servers
|
||||||
handshakes cannot carry response headers, so on RSGI the session is loaded and
|
in practice — including **Granian** and **Daphne** — do not forward them into
|
||||||
persisted but the cookie is only set or refreshed by HTTP responses.
|
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
|
## Session expiry
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user