WebSocket¶
Una ruta WebSocket propia se protege con una dependencia, igual que un endpoint HTTP. No hace
falta saber cómo viaja el token ni qué se comprueba: la conexión llega al endpoint ya aceptada y
autenticada, con el mismo AccessToken que en HTTP.
En la API¶
# api/routers/custom/chat.py
from fastapi import APIRouter, Depends, WebSocket
from api.routers.auth import (
AccessToken,
websocket_requires_authentication,
websocket_requires_permissions,
)
router = APIRouter(prefix="/ws", tags=["Tiempo real"])
@router.websocket("/avisos")
async def avisos(websocket: WebSocket, token: AccessToken = Depends(websocket_requires_authentication())):
await websocket.send_json({"hola": token.preferred_username})
async for mensaje in websocket.iter_text():
...
@router.websocket("/pedidos")
async def pedidos(websocket: WebSocket, token: AccessToken = Depends(websocket_requires_permissions("read", "pedido"))):
...
| Dependencia | Exige | Equivale en HTTP a |
|---|---|---|
websocket_requires_authentication() |
Un usuario autenticado | requires_authentication() |
websocket_requires_permissions(action, resource) |
El permiso <resource>-<action> (o sudo, <resource>-admin, allresources-<action>) |
requires_permissions(action, resource) |
Las dos funcionan igual con Database y con Keycloak, y respetan RBAC_ENABLED: con tai-api dev
se entra sin token, con el usuario de desarrollo.
No llames a accept()
La dependencia acepta la conexión antes de que empiece el endpoint. Si tu código ya llamaba a
websocket.accept() —un gestor de conexiones, por ejemplo— no hace falta quitarlo: la segunda
llamada no hace nada.
Con from __future__ import annotations, importa WebSocket arriba del módulo
FastAPI busca la anotación websocket: WebSocket entre los nombres del módulo. Si WebSocket
se importa dentro de una función, la toma por un parámetro de la URL y rechaza la conexión.
En el navegador¶
Con el cliente TypeScript y la sesión con cookie:
import { createTaiClient, shouldReconnect } from '@/lib/api'
const api = createTaiClient()
async function conectar() {
const ws = await api.session.openWebSocket('/ws/avisos')
ws.onmessage = (e) => console.log(JSON.parse(e.data))
ws.onclose = (e) => {
if (shouldReconnect(e.code)) setTimeout(conectar, 1_000)
}
}
openWebSocket refresca el token si está a punto de caducar, lo pone en la conexión y resuelve la
URL igual que las peticiones (/api detrás del proxy del frontend). taiApiProxy() deja pasar los
WebSocket (ws: true).
Sin el cliente, el token va como subprotocolo, detrás de tai.bearer:
Un cliente que no es un navegador puede mandar en su lugar Authorization: Bearer <token>.
Nunca en la URL
?token=... acaba en los logs de acceso de uvicorn y de cualquier proxy que haya delante. La
API no lo lee.
Cuando no entra¶
La conexión se acepta y se cierra con un código. Cerrarla antes de aceptarla no serviría: el
navegador solo vería un 1006, sin código ni motivo.
| Código | Motivo (reason) |
Qué hacer |
|---|---|---|
4401 |
UNAUTHORIZED_ACCESS, INVALID_TOKEN, SESSION_INVALIDATED |
Iniciar sesión otra vez |
4419 |
TOKEN_EXPIRED |
Reconectar: openWebSocket pide un token vigente |
4403 |
INSUFFICIENT_PERMISSIONS, ORIGIN_NOT_ALLOWED |
Nada: reconectar no lo arregla |
1013 |
SERVICE_UNAVAILABLE (Keycloak no responde) |
Reintentar más tarde |
shouldReconnect(code) reconecta con 1006, 4419 y 1013. Los códigos que cierre tu propio
endpoint (un recurso que no existe, uno que no es del usuario) son tuyos: elige otros del rango
4000–4999 y no los confundas con estos.
El origen¶
CORS no se aplica a WebSocket, así que la dependencia comprueba la cabecera Origin contra la
misma lista, ALLOWED_ORIGINS de config.py:
ALLOWED_ORIGINS |
Qué se acepta |
|---|---|
["*"] |
Cualquier origen |
["https://app.cliente.com"] |
Esos orígenes y el de la propia API |
[] (sesión con cookie detrás del proxy /api) |
El mismo origen, también el que llega en X-Forwarded-Host, o PUBLIC_URL |
Una conexión sin Origin no viene de un navegador y no se comprueba.
Lo que no hace (todavía)¶
- Revalidar durante la conexión. El token se comprueba al conectar. Si la sesión se cierra
después —logout, otra sesión, un cambio de contraseña—, la conexión abierta sigue hasta que se
cierre. Si el endpoint vive mucho, ciérralo tú periódicamente con
4419: el cliente reconectará con un token vigente, y si la sesión ya no existe, recibirá4401.