
O WebSocket que não apertava a mão — e os 5 endpoints que ficaram mudos
⚡ O handshake que nunca acontecia
Era o tipo de bug que não faz barulho. O Dogwalk rodava, o banco respondia, o deploy tava verde. Mas o WebSocket de notificações — e todos os outros 4 — retornavam HTTP 500 no handshake.
Notificações em tempo real, chat com o walker, rastreamento ao vivo do passeio, atualização de status: tudo mudo. Ninguém gritava, porque o erro aparecia como 500 no console do navegador e todo mundo seguia a vida.
🧠 O contexto: WebSocket no Starlette tem um protocolo de aperto de mão
No Starlette (o coração do FastAPI), um WebSocket não é só “abrir e falar”. Tem uma ordem obrigatória:
# ❌ O que o código fazia (implicitamente)
async def await_ws_auth(websocket: WebSocket) -> Optional[dict]:
raw = await websocket.receive_text() # ← ESCUTA ANTES DE ACEITAR
...
O Starlette exige accept() antes de receive() ou send(). É o aperto de mão: primeiro você aceita a conexão, depois conversa. Chamar receive_text() sem aceitar é como atender o telefone e sair falando sem dizer “alô”.
# ✅ O que o Starlette exige
async def await_ws_auth(websocket: WebSocket) -> Optional[dict]:
await websocket.accept() # ← ALÔ
raw = await asyncio.wait_for(websocket.receive_text(), timeout=10)
...
🔧 A luta: 5 endpoints afetados, um padrão
O pior: o bug não estava em 1 lugar. Estava espalhado por 5 rotas que compartilhavam o mesmo helper await_ws_auth():
| Endpoint | Função |
|---|---|
/ws/notifications |
Notificações em tempo real |
/ws/walk/{id} |
Atualização ao vivo do passeio |
/ws/chat/{id} |
Chat com o walker |
/walks/{id}/status |
Mudança de status |
/ws/walk-now/{id} |
Rastreamento do passeio |
Todas chamavam await_ws_auth() no início. Uma falha no helper = 5 endpoints mudos.
O handshake HTTP 500 não era óbvio no teste de health — porque o /health é HTTP normal. O WebSocket só aparecia quando o navegador tentava conectar, e aí… silêncio.
💡 A resolução: uma linha que destravou tudo
# backend/app/routers/ws.py
async def await_ws_auth(websocket: WebSocket) -> Optional[dict]:
# 🔴 FIX 03/08/2026: Starlette exige accept() ANTES de receive_text/send_json.
await websocket.accept()
try:
raw = await asyncio.wait_for(websocket.receive_text(), timeout=10)
...
Uma linha: await websocket.accept() no início. Os 5 endpoints voltaram a funcionar.
Verificação real: 6/6 WebSocket via tunnel OK + todos os 5 endpoints OK + pytest 32/32.
📊 Métricas
| Métrica | Valor |
|---|---|
| Endpoints afetados | 5 |
| Linhas do fix | 1 (await websocket.accept()) |
| Testes pós-fix | 32/32 pytest |
| WebSocket via tunnel | 6/6 OK |
| Endpoints verificados | 5/5 |
🎯 Aprendizados
-
WebSocket tem protocolo de aperto de mão — Starlette exige
accept()antes dereceive()/send(). É o “alô” do telefone. Sem ele, handshake vira HTTP 500 silencioso. -
Bug de shared helper = N endpoints quebrados — o
await_ws_auth()era usado por 5 rotas. Um erro num helper compartilhado não quebra 1 feature, quebra o domínio inteiro. Audit helpes usados em múltiplos endpoints quando algo “não conecta”. -
Health check HTTP não pega WebSocket — o
/health(HTTP normal) passava. O WS só falhava no navegador. Health checks precisam incluir handshakes WS se o app depende de tempo real. -
Bug silencioso é o mais perigoso — HTTP 500 no console do navegador não apita alerta. O sistema “funciona” (páginas carregam) mas features críticas estão mudas.