O WebSocket que não apertava a mão — e os 5 endpoints que ficaram mudos
🐶 Dogwalk·

O WebSocket que não apertava a mão — e os 5 endpoints que ficaram mudos

📖 5 min de leitura← Voltar para timeline

⚡ 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

  1. WebSocket tem protocolo de aperto de mão — Starlette exige accept() antes de receive()/send(). É o “alô” do telefone. Sem ele, handshake vira HTTP 500 silencioso.

  2. 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”.

  3. 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.

  4. 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.

~/lifelog — bash
$cat about.txt
╔══════════════════════════════════════╗
║  Samuel Medeiros                    ║
║  Senior Software Engineer           ║
║  Stack: Python · TypeScript · Rust  ║
║  Projetos: Arachne, Dogwalk,        ║
║            Capivara, TatuEngine      ║
╚══════════════════════════════════════╝
      
$