Como depurar problemas de rede em aplicações Node

Problemas de rede em aplicações Node.js podem ser particularmente desafiadores devido à natureza assíncrona e orientada a eventos da plataforma. Este artigo apresenta um guia prático para diagnosticar e resolver os problemas de rede mais comuns, desde o rastreamento básico de requisições até a análise avançada de tráfego.

1. Diagnóstico Inicial com Ferramentas Nativas do Node

Antes de recorrer a ferramentas externas, o Node oferece recursos nativos poderosos para depuração de rede. O módulo util com inspect permite visualizar objetos complexos de requisições HTTP:

const http = require('http');
const util = require('util');

const server = http.createServer((req, res) => {
  console.log('Requisição recebida:');
  console.log(util.inspect(req.headers, { showHidden: true, depth: null }));
  res.end('OK');
});

server.listen(3000);

Para ativar logs detalhados de rede, use a variável de ambiente NODE_DEBUG:

NODE_DEBUG=net,http node app.js

Isso exibirá todas as operações de socket e requisições HTTP, incluindo conexões abertas, dados enviados/recebidos e eventos de fechamento.

2. Análise de Timeouts e Conexões Pendentes

Timeouts mal configurados são uma das principais causas de problemas de rede. O Node permite configurar timeouts em múltiplos níveis:

const server = http.createServer((req, res) => {
  // Timeout por requisição
  req.setTimeout(5000, () => {
    console.error('Timeout da requisição');
    res.statusCode = 408;
    res.end('Request Timeout');
  });

  // Simula processamento lento
  setTimeout(() => {
    res.end('Processado');
  }, 10000);
});

server.timeout = 10000; // Timeout global do servidor
server.listen(3000);

Para detectar sockets vazados (não encerrados), utilize process._getActiveHandles():

setInterval(() => {
  const handles = process._getActiveHandles();
  const sockets = handles.filter(h => h.constructor.name === 'Socket');
  console.log(`Sockets ativos: ${sockets.length}`);
  sockets.forEach((s, i) => {
    console.log(`Socket ${i}: ${s.remoteAddress}:${s.remotePort}`);
  });
}, 5000);

Sempre encerre sockets corretamente em caso de erro:

socket.on('error', (err) => {
  console.error('Erro no socket:', err.message);
  socket.destroy(); // Garante o fechamento
});

3. Depuração de DNS e Resolução de Hostnames

Problemas de DNS podem causar falhas intermitentes difíceis de diagnosticar. Teste a resolução diretamente com o módulo nativo dns:

const dns = require('dns');

// Teste de resolução
dns.resolve('api.exemplo.com', 'A', (err, addresses) => {
  if (err) {
    console.error('Falha na resolução DNS:', err.code);
    // Fallback para IP estático
    console.log('Usando IP de fallback: 192.168.1.100');
  } else {
    console.log('Endereços resolvidos:', addresses);
  }
});

// Configurar servidores DNS personalizados
dns.setServers(['8.8.8.8', '1.1.1.1']);

Para cache de DNS, implemente um mecanismo simples:

const dnsCache = new Map();
const cacheTTL = 300000; // 5 minutos

function resolveWithCache(hostname) {
  const cached = dnsCache.get(hostname);
  if (cached && (Date.now() - cached.timestamp) < cacheTTL) {
    return Promise.resolve(cached.address);
  }

  return new Promise((resolve, reject) => {
    dns.resolve(hostname, (err, addresses) => {
      if (err) reject(err);
      else {
        dnsCache.set(hostname, { address: addresses[0], timestamp: Date.now() });
        resolve(addresses[0]);
      }
    });
  });
}

4. Rastreamento de Requisições HTTP/HTTPS com Interceptação

Para depurar chamadas HTTP sem modificar o código original, crie um proxy de interceptação:

const http = require('http');

// Proxy de interceptação
const proxy = http.createServer((req, res) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  console.log('Headers:', JSON.stringify(req.headers, null, 2));

  let body = '';
  req.on('data', chunk => body += chunk);
  req.on('end', () => {
    if (body) console.log('Body:', body);

    // Encaminha para o servidor real
    const options = {
      hostname: 'api.exemplo.com',
      port: 80,
      path: req.url,
      method: req.method,
      headers: req.headers
    };

    const proxyReq = http.request(options, (proxyRes) => {
      console.log('Status:', proxyRes.statusCode);
      res.writeHead(proxyRes.statusCode, proxyRes.headers);
      proxyRes.pipe(res);
    });

    proxyReq.write(body);
    proxyReq.end();
  });
});

proxy.listen(8080, () => {
  console.log('Proxy de depuração rodando na porta 8080');
});

5. Monitoramento de Tráfego com Ferramentas Externas

Para análise avançada, integre o Node com ferramentas como tcpdump. Crie um script que capture tráfego específico:

const { exec } = require('child_process');

function captureTraffic(port, duration = 30) {
  const cmd = `sudo tcpdump -i any port ${port} -w captura_${port}.pcap`;
  console.log(`Capturando tráfego na porta ${port} por ${duration}s...`);

  const capture = exec(cmd);

  setTimeout(() => {
    capture.kill();
    console.log('Captura concluída. Arquivo: captura_' + port + '.pcap');
    console.log('Analise com: wireshark captura_' + port + '.pcap');
  }, duration * 1000);
}

// Uso: captureTraffic(3000, 60);

Para testes isolados, integre chamadas curl no Node:

const { execSync } = require('child_process');

function testEndpoint(url) {
  try {
    const result = execSync(`curl -v --connect-timeout 5 --max-time 10 ${url}`, {
      encoding: 'utf8',
      maxBuffer: 1024 * 1024
    });
    console.log('Resposta:', result);
  } catch (error) {
    console.error('Falha na requisição:', error.stderr);
  }
}

testEndpoint('http://localhost:3000/api/health');

6. Debug de WebSockets e Conexões Persistentes

WebSockets exigem atenção especial devido à natureza persistente da conexão:

const WebSocket = require('ws');

const ws = new WebSocket('ws://localhost:8080');

ws.on('open', () => {
  console.log('[WS] Conexão estabelecida');
  console.log('[WS] Protocolo:', ws.protocol);
  console.log('[WS] Extensões:', ws.extensions);
});

ws.on('message', (data) => {
  console.log('[WS] Mensagem recebida:', data.toString());
});

ws.on('close', (code, reason) => {
  console.log(`[WS] Conexão fechada - Código: ${code}, Razão: ${reason}`);
  // Reconexão automática
  setTimeout(() => {
    console.log('[WS] Tentando reconectar...');
    // Lógica de reconexão
  }, 5000);
});

ws.on('error', (error) => {
  console.error('[WS] Erro:', error.message);
  console.error('[WS] Stack:', error.stack);
});

7. Isolamento de Problemas com Containers e Load Balancers

Para simular falhas de rede em ambientes containerizados, use tc (traffic control) via Node:

const { execSync } = require('child_process');

function simulateLatency(containerName, latencyMs = 200) {
  const cmd = `docker exec ${containerName} tc qdisc add dev eth0 root netem delay ${latencyMs}ms`;
  try {
    execSync(cmd);
    console.log(`Latência de ${latencyMs}ms aplicada ao container ${containerName}`);
  } catch (error) {
    console.error('Erro ao simular latência:', error.message);
  }
}

function simulatePacketLoss(containerName, lossPercent = 10) {
  const cmd = `docker exec ${containerName} tc qdisc add dev eth0 root netem loss ${lossPercent}%`;
  try {
    execSync(cmd);
    console.log(`Perda de ${lossPercent}% aplicada ao container ${containerName}`);
  } catch (error) {
    console.error('Erro ao simular perda:', error.message);
  }
}

// Uso: simulateLatency('api-container', 500);

Configure corretamente o http.Agent para gerenciar conexões:

const http = require('http');

const agent = new http.Agent({
  keepAlive: true,
  keepAliveMsecs: 1000,
  maxSockets: 50,
  maxFreeSockets: 10,
  timeout: 60000
});

const options = {
  hostname: 'api.exemplo.com',
  port: 80,
  path: '/',
  agent: agent
};

const req = http.get(options, (res) => {
  console.log('Conexão reutilizada:', res.socket.isSessionReused());
});

8. Automação de Diagnósticos com Scripts e Logs Estruturados

Crie um script de monitoramento contínuo que gera logs JSON para integração com ELK:

const http = require('http');
const fs = require('fs');

function monitorEndpoint(url, interval = 5000) {
  const logStream = fs.createWriteStream('network_monitor.log', { flags: 'a' });

  setInterval(() => {
    const start = Date.now();
    const logEntry = {
      timestamp: new Date().toISOString(),
      url: url,
      type: 'health_check'
    };

    http.get(url, (res) => {
      const duration = Date.now() - start;
      logEntry.status = res.statusCode;
      logEntry.duration_ms = duration;
      logEntry.success = res.statusCode >= 200 && res.statusCode < 400;

      logStream.write(JSON.stringify(logEntry) + '\n');
      console.log(`[${logEntry.timestamp}] ${url} - ${logEntry.status} (${duration}ms)`);
    }).on('error', (err) => {
      logEntry.status = 0;
      logEntry.error = err.message;
      logEntry.success = false;

      logStream.write(JSON.stringify(logEntry) + '\n');
      console.error(`[${logEntry.timestamp}] ${url} - ERRO: ${err.message}`);
    });
  }, interval);
}

monitorEndpoint('http://localhost:3000/health');

Para debugging remoto com breakpoints condicionais:

// Inicie com: node --inspect app.js
// Depois acesse: chrome://inspect

function processRequest(data) {
  // Breakpoint condicional: data.statusCode >= 500
  if (data.statusCode >= 500) {
    debugger; // Só para quando houver erro 5xx
    console.error('Erro no servidor:', data);
  }

  // Lógica normal
  return data;
}

Referências