Skip to main content

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:

  1. Headers
  2. 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

  1. Client envia a requisição
  2. Nginx repassa para o backend
  3. Nginx aguarda os headers HTTP da resposta
  4. Backend fecha o socket abruptamente
  5. O sistema operacional retorna ECONNRESET
  6. 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 ConnectionAbortedException internamente
  • 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