Erro 104 no NGINX com .NET - Visão 1 | (104: Connection reset by peer) while reading response header from upstream | Ambiente .NET
- Erro 104 no NGINX com .NET - Visão 1 | (104: Connection reset by peer) while reading response header from upstream | Ambiente .NET
Erro 104: Connection reset by peer no Nginx com SignalR
Visão geral do problema
O erro abaixo aparece nos logs do Nginx quando ele atua como proxy reverso de uma aplicação backend (ASP.NET Core com SignalR):
(104: Connection reset by peer) while reading response header from upstream
Esse erro significa que o Nginx enviou a requisição corretamente, mas a aplicação backend fechou a conexão TCP de forma abrupta, antes de retornar qualquer resposta HTTP válida.
O Nginx não causou o erro. Ele apenas detectou que a conexão foi encerrada pelo servidor upstream.
O que são os headers de resposta HTTP
Antes de entender o erro, é essencial entender o que o Nginx espera receber.
Uma resposta HTTP é composta por duas partes principais:
- Headers
- Body
Exemplo de resposta HTTP válida:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 123
Connection: keep-alive
{ "status": "ok" }
Os headers:
- São enviados primeiro
- Informam status HTTP, tipo de conteúdo, tamanho, etc
- Precisam chegar completos antes do body
Quando o log diz:
while reading response header from upstream
Significa que:
- O Nginx ainda não recebeu nem o status HTTP
- Nenhum header foi escrito no socket
- A conexão foi encerrada no meio do caminho
Isso caracteriza um abort de conexão, não um erro HTTP.
Fluxo técnico do erro 104
- Client envia a requisição
- Nginx repassa para o backend
- Nginx aguarda os headers HTTP da resposta
- Backend fecha o socket abruptamente
- O sistema operacional retorna
ECONNRESET - Nginx registra o erro 104
Nesse fluxo, nenhum HTTP 4xx ou 5xx é gerado, pois a resposta nunca existiu.
Por que o SignalR é mais sensível a esse erro
O endpoint:
POST /{hub}/negotiate
é responsável por:
- Autenticar o client
- Gerar
connectionId - Definir o transporte (WebSocket, SSE, Long Polling)
Esse endpoint:
- É síncrono do ponto de vista HTTP
- Precisa responder rápido
- Não tolera bloqueios internos
Se o /negotiate não consegue escrever os headers rapidamente, a conexão pode ser abortada.
Causas internas mais comuns no backend
Saturação do pool de conexões (MongoDB)
Exemplo de código problemático durante o handshake:
public override async Task OnConnectedAsync()
{
var user = await _usersCollection
.Find(u => u.Id == Context.UserIdentifier)
.FirstAsync();
}
O que acontece sob carga:
- Todas as conexões do pool Mongo ficam ocupadas
- A thread entra na fila interna do driver
- Nenhum header HTTP é enviado
- A request fica bloqueada
Se o tempo de espera estoura:
- A requisição é cancelada
- O socket é fechado
- O Nginx registra o erro 104
Thread Pool Starvation
Exemplo de padrão perigoso:
var result = Task.Run(() => DoWorkAsync()).Result;
Ou:
await Task.WhenAll(requests.Select(r => CallMongoAsync(r)));
Sob alta concorrência:
- Threads ficam bloqueadas
- Continuations async não são executadas
- A requisição não progride
- Headers nunca são escritos
O backend encerra a conexão para liberar recursos.
Pressão de Garbage Collector
Exemplo típico:
- DTOs grandes enviados no SignalR
- Serialização excessiva no handshake
- Alta taxa de alocação por request
Consequências:
- Pausas de GC
- Threads suspensas temporariamente
- Escrita da resposta atrasada
Durante o /negotiate, esse atraso pode causar o abort da conexão.
Comportamento do Kestrel
Quando uma requisição:
- Fica aberta por tempo excessivo
- Não consegue avançar
- Não escreve headers
O Kestrel pode:
- Cancelar a requisição
- Lançar
ConnectionAbortedExceptioninternamente - Fechar o socket imediatamente
Esse fechamento não envia resposta HTTP, apenas encerra a conexão TCP.
Papel do Nginx no cenário
O Nginx:
- Envia a requisição corretamente
- Aguarda os headers HTTP
- Detecta o fechamento abrupto
- Registra
(104: Connection reset by peer)
O Nginx não tem como transformar isso em HTTP 500, pois nunca recebeu uma resposta válida.
Boas práticas para evitar o erro
Manter o /negotiate leve e previsível
Evitar dependências externas durante o handshake.
Exemplo incorreto:
public override async Task OnConnectedAsync()
{
var permissions = await _mongo.Permissions
.Find(p => p.UserId == Context.UserIdentifier)
.ToListAsync();
}
Exemplo correto:
public override Task OnConnectedAsync()
{
return base.OnConnectedAsync();
}
Dados devem ser carregados após a conexão estar estabelecida.
Limitar concorrência no handshake
Protege o backend contra avalanche de conexões.
static SemaphoreSlim limiter = new(50);
public override async Task OnConnectedAsync()
{
await limiter.WaitAsync();
try
{
await base.OnConnectedAsync();
}
finally
{
limiter.Release();
}
}
Isso garante:
- Número controlado de handshakes
- Menor pressão no pool e no ThreadPool
Configurar timeouts para falhar rápido
MongoDB:
settings.WaitQueueTimeout = TimeSpan.FromSeconds(2);
settings.SocketTimeout = TimeSpan.FromSeconds(30);
Kestrel:
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(10);
Esses limites evitam conexões presas indefinidamente.
Configuração adequada do Nginx
Garantir que o proxy não desista prematuramente:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 300s;
proxy_send_timeout 300s;
Mesmo assim, se o backend abortar a conexão, o erro 104 ainda será registrado.
Diagnóstico do problema
Para identificar a causa real:
- Correlacionar timestamp do erro no Nginx com logs da aplicação
- Procurar
MongoWaitQueueFullException - Procurar
ConnectionAbortedException - Monitorar uso de ThreadPool e GC
- Medir tempo de resposta do
/negotiate
Resumo técnico do comportamento
- O erro 104 indica encerramento abrupto da conexão pelo backend
- Nenhum header HTTP foi enviado
- O SignalR é sensível durante o
/negotiate - Saturação interna leva o Kestrel a abortar o socket
- O Nginx apenas registra o reset observado