Guia de Reconexão WebSocket — Integradores¶
Fluxo esperado, códigos de fechamento e boas práticas de implementação
Documento para integradores
Este guia explica como o SADI Online se comporta durante quedas de conexão, reconexões e troca de conexão ativa — e o que implementar no cliente.
Visão geral¶
O streaming WebSocket trabalha com o conceito de sessão, identificada pelo session_id (UUID). A sessão permanece ativa mesmo quando a conexão WebSocket cai temporariamente.
| Conceito | Descrição |
|---|---|
| Sessão | Identificada pelo session_id retornado na conexão |
| Conexão WebSocket | Canal temporário; pode cair e ser reaberto |
| Janela de reconexão | Ilimitada por padrão; configurável para um prazo finito |
| Seleção da sessão | O session_id explícito pode apontar para uma sessão histórica do próprio estudante |
| Substituição de conexão | Nova conexão assume a sessão; antiga recebe 4007 |
| Intervalo mínimo | 5s entre reconexões bem-sucedidas consecutivas |
Regra de ouro
Guarde o session_id retornado na conexão inicial e use-o em todas as reconexões. Mesmo após POST /sessions/{id}/end/, é possível reativar a mesma sessão reconectando com o mesmo session_id. Só inicie sessão nova (sem session_id) quando quiser descartar o histórico desta sessão.
Conexão nova (primeira vez)¶
Cliente → wss://ws.../v1/stream/?student_id=...&activity_id=...
→ autenticação (API Key)
→ servidor cria nova sessão
→ {"status": "connected", "session_id": "uuid..."}
| Parâmetro | Obrigatório | Descrição |
|---|---|---|
student_id | Sim | UUID do estudante |
activity_id | Sim (Enterprise) | UUID da atividade |
session_id | Não | Omitido = nova sessão |
Armazene o session_id retornado para reconexões futuras.
Reconexão com session_id¶
Cliente → wss://...?student_id=...&activity_id=...&session_id={uuid}
→ servidor valida sessão ativa e pertencente ao estudante
→ conexão antiga (se existir) recebe 4007
→ nova conexão aceita
→ {"status": "connected", "session_id": "mesmo-uuid"}
Retome o envio de frames após receber a confirmação. Não é necessário reenviar frames anteriores.
Erros comuns ao reconectar (4001)¶
| Motivo no JSON | O que fazer |
|---|---|
reconnect_window_expired | Nova sessão (somente quando o grace finito configurado expirou) |
reconnect_cooldown_active | Aguardar 5s e tentar novamente |
session_open_conflict | Outra sessão do estudante ainda está aberta; finalizar ou retomar essa sessão antes |
reactivate_not_ended | Sessão não está encerrada; verificar estado via API |
Disconnect normal e política de reconexão¶
Quando a conexão cai (1000, 1006, timeout):
- A sessão entra em
RECONNECTINGe conserva o mesmosession_id - No padrão (
DJANGO_WS_RECONNECT_GRACE_SECONDS=0), ela pode ser reconectada sem prazo - Com
DJANGO_WS_RECONNECT_GRACE_SECONDS=60(modo legado), ela é encerrada se o deadline expirar
Durante uma queda transitória, não chame POST /sessions/{id}/end/.
O intervalo offline não entra na duração efetiva nem no billing. Uma sessão em RECONNECTING continua sendo uma sessão aberta e bloqueia a criação de outra sessão para o mesmo estudante. Para retenção operacional opcional, configure DJANGO_WS_RECONNECT_MAX_AGE_DAYS com um valor maior que zero; o sweeper encerra sessões abandonadas acima desse limite e libera a criação de uma nova sessão.
O servidor não exige que a sessão escolhida seja a mais recente. O cliente deve enviar o session_id da sessão que deseja retomar. Se outra sessão do estudante já estiver aberta, a retomada é rejeitada com session_open_conflict; o servidor não encerra a sessão aberta automaticamente.
Reconexão após POST /sessions/{id}/end/¶
Chamar /end/ marca a sessão como ENDED e fecha o WebSocket ativo com 4002 (se ainda houver conexão). Isso não impede reconectar depois com o mesmo session_id.
POST /sessions/{id}/end/ → status ENDED, socket recebe 4002
↓
Usuário quer continuar
↓
WebSocket com o mesmo session_id → sessão reativada (status ACTIVE)
↓
{"status": "connected", "session_id": "mesmo-uuid"}
Ação após /end/ | Funciona? | Resposta |
|---|---|---|
WebSocket com o mesmo session_id | Sim | Sessão reativada; retoma frames |
POST /pause/ | Não | HTTP 409 (session_already_ended) — reconecte antes de pausar |
WebSocket sem session_id | Sim | Cria sessão nova (histórico anterior fica encerrado) |
Dica de implementação
Ao receber 4002 após /end/, não descarte o session_id se o usuário puder querer continuar a mesma aula. Mostre "Retomar" e reconecte com o mesmo session_id. Descarte o session_id só quando o usuário iniciar uma atividade nova de fato.
O tempo entre o /end/ e a reconexão não entra na duração efetiva nem na cobrança. Apenas o tempo com a sessão ativa (antes e depois do encerramento) conta.
Código 4007 — Substituição de conexão¶
Significa que outra conexão assumiu esta sessão. Comportamento esperado, não é erro.
Ao receber 4007:
- Pare de enviar frames nesta conexão
- Não reconecte automaticamente nesta instância/aba
- Não chame
POST /sessions/{id}/end/ - A sessão continua ativa na outra conexão
Código 4002 — Sessão encerrada¶
Significa que a sessão foi encerrada nesta conexão e não aceita mais frames por aqui.
Causas comuns:
- Integrador chamou
POST /sessions/{id}/end/ - Expirou o deadline configurado em modo finito (
DJANGO_WS_RECONNECT_GRACE_SECONDS>0) - Outro sistema encerrou a sessão via API
Erro frequente
Conexão cai → integrador chama POST /end/ no onclose → sessão encerra → socket recebe 4002. A sessão ainda pode ser reativada com o mesmo session_id se o usuário continuar a atividade.
Solução: nunca chame /end/ automaticamente no onclose.
Ao receber 4002:
- Pare de enviar frames nesta conexão
- Se o usuário pode continuar a mesma atividade: guarde o
session_ide reconecte com ele (reativa a sessão) - Se o usuário encerrou de fato: descarte o
session_ide conecte sem ele para nova sessão
Tabela de códigos de fechamento¶
| Código | Significado | Sessão encerra? | O que fazer |
|---|---|---|---|
1000 | Fechamento normal | Não (grace ilimitado por padrão) | Reconectar com session_id |
1006 | Conexão perdida | Não (grace ilimitado por padrão) | Reconectar com session_id |
4001 | Falha create/reconnect | — | Ver motivo no JSON |
4002 | Sessão encerrada nesta conexão | Sim (até reativar) | Reconectar com session_id para continuar, ou nova sessão |
4003 | Autenticação inválida | — | Verificar API Key |
4004 | Limite de uso excedido | Sim | Contatar suporte |
4005 | Frames inválidos | Sim | Corrigir formato |
4006 | Device revogado | Sim | Reautenticar |
4007 | Outra conexão assumiu | Não | Parar; não reconectar nem chamar /end |
O que NÃO fazer¶
| Comportamento incorreto | Consequência |
|---|---|
POST /end/ no onclose após queda de rede | Sessão encerra; pode reativar com session_id ou criar nova |
| Auto-reconnect na aba que recebeu 4007 | Loop de troca de conexão |
Reconectar sem session_id quando quer continuar | Nova sessão; histórico perdido |
| Múltiplas abas na mesma sessão | Troca constante (4007) |
Quando chamar POST /sessions/{id}/end/
Somente quando o usuário encerra explicitamente a atividade. Nunca no onclose do WebSocket.
Implementação recomendada no cliente¶
let sessionId = null;
let reconnectAttempts = 0;
let isSuperseded = false;
let shouldEndSession = false;
function onClose(event) {
switch (event.code) {
case 4007:
isSuperseded = true;
return;
case 4002:
// sessão encerrada nesta conexão; manter sessionId se o usuário puder retomar
return;
case 4001:
// verificar reason no JSON de erro
return;
case 1000:
case 1006:
default:
if (sessionId && !shouldEndSession) {
reconnectWithBackoff();
}
return;
}
}
async function endSessionByUser() {
shouldEndSession = true;
ws.close(1000, 'User ended session');
await fetch(`/tenant/${orgId}/v1/sessions/${sessionId}/end/`, {
method: 'POST',
headers: { 'Authorization': `APIKey ${apiKey}` },
});
sessionId = null;
}
Troubleshooting¶
4002 logo após /end/: comportamento esperado. Reconecte com o mesmo session_id se o usuário quiser continuar.
4002 após queda de rede + /end/ no onclose: remova /end/ automático do onclose; use reconnect transitório (1000/1006) em vez disso.
Loop de 4007: duas abas disputando a mesma sessão — apenas uma deve manter conexão.
reconnect_cooldown_active: aguarde 5s após reconnect bem-sucedido.
reconnect_window_expired: o deadline finito expirou — criar nova sessão.
No modo padrão, esse erro não é produzido por passagem de tempo. Se a sessão continuar em RECONNECTING, reconecte usando o mesmo session_id ou encerre-a explicitamente; não crie outra sessão sem session_id esperando que a anterior seja substituída.
Documentos relacionados¶
SADI Online — Instituto Anexo · 23 de junho de 2026