Undici é o cliente HTTP de alto desempenho que serve de base para o Fetch disponível no Node.js. A biblioteca oferece APIs de baixo e alto nível para controlar conexões, pools, pipelining, streaming, timeouts e dispatchers.
Para chamadas simples, o Fetch global costuma ser suficiente. Undici se torna útil quando a aplicação precisa ajustar pool por origem, monitorar conexões, usar pipelines, construir um cliente reutilizável ou obter desempenho previsível em grande volume.
Instalação
npm install undiciEmbora o Node.js use uma versão interna para Fetch, instalar o pacote permite acessar APIs específicas e controlar a versão pelo projeto.
request
import { request } from 'undici';
const { statusCode, headers, body } = await request(
'https://api.example.com/users/42',
{
method: 'GET',
headers: {
accept: 'application/json',
},
},
);
if (statusCode !== 200) {
body.resume();
throw new Error(`HTTP ${statusCode}`);
}
const data = await body.json();
console.log(data);O status HTTP não gera exceção automaticamente. Verifique o código e sempre consuma ou descarte o corpo para liberar a conexão.
Client
Client representa uma conexão ou conjunto controlado para uma origem.
import { Client } from 'undici';
const client = new Client('https://api.example.com', {
connections: 10,
pipelining: 1,
});
const response = await client.request({
path: '/health',
method: 'GET',
});
console.log(response.statusCode);
await response.body.text();Reutilize o cliente. Criar uma instância por requisição elimina os benefícios do pool.
Pool
Pool gerencia várias conexões para a mesma origem:
import { Pool } from 'undici';
const pool = new Pool('https://api.example.com', {
connections: 20,
pipelining: 1,
});
const { body } = await pool.request({
path: '/items',
method: 'GET',
});
const items = await body.json();Dimensione connections pela concorrência, latência da dependência, quantidade de réplicas e limites externos.
Pipelining
HTTP pipelining envia várias requisições na mesma conexão sem aguardar respostas anteriores. Ele pode aumentar throughput, mas respostas seguem em ordem. Uma chamada lenta pode atrasar as seguintes.
Use valores maiores somente para operações idempotentes e serviços conhecidos. Faça benchmark com latência real e falhas.
Dispatcher
Dispatcher é a abstração central de envio. Client, Pool e Agent implementam essa interface. Funções podem receber um dispatcher sem conhecer a configuração completa.
async function fetchUser(id, { dispatcher, signal }) {
const response = await fetch(`https://api.example.com/users/${id}`, {
dispatcher,
signal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}Agent
Um Agent cria pools por origem:
import { Agent } from 'undici';
const agent = new Agent({
connections: 20,
pipelining: 1,
});Ele é útil quando a aplicação acessa diversas origens conhecidas. Ainda assim, imponha allowlist para URLs externas.
Dispatcher global
import {
Agent,
setGlobalDispatcher,
} from 'undici';
setGlobalDispatcher(new Agent({
connections: 50,
keepAliveTimeout: 10_000,
}));Alterar globalmente afeta chamadas Fetch que usam o dispatcher padrão. Faça isso no bootstrap, documente e evite bibliotecas alterando estado global.
Timeouts
Timeouts podem envolver cabeçalhos, corpo, conexão e deadline total. Use AbortSignal para o orçamento da operação:
const signal = AbortSignal.timeout(5_000);
const response = await fetch(url, {
dispatcher: pool,
signal,
});O timeout deve ser menor que o prazo do chamador. Diferencie timeout de cabeçalhos e de corpo quando precisar diagnosticar.
Headers timeout
Uma dependência pode aceitar a conexão e demorar para enviar cabeçalhos. Configure um limite coerente no client ou pool e monitore o erro específico.
Body timeout
Depois dos cabeçalhos, o corpo pode parar. Limite inatividade e tamanho. Uma resposta que envia um byte periodicamente pode evitar um timeout de inatividade, por isso também use deadline total.
Streaming de resposta
import { pipeline } from 'node:stream/promises';
import { createWriteStream } from 'node:fs';
const { body, statusCode } = await pool.request({
path: '/exports/latest',
method: 'GET',
});
if (statusCode !== 200) {
body.resume();
throw new Error(`HTTP ${statusCode}`);
}
await pipeline(body, createWriteStream('export.csv'));Pipeline aplica backpressure e fecha recursos em erros.
Enviando stream
O corpo de uma requisição pode ser string, Buffer, iterable ou stream conforme a API. Não informe Content-Length incorreto. Para uploads grandes, use backpressure e cancelamento.
FormData
const form = new FormData();
form.set('name', 'arquivo');
form.set('file', new Blob([buffer]), 'data.bin');
const response = await fetch(url, {
method: 'POST',
body: form,
dispatcher: pool,
});Para arquivos enormes, evite acumular todo conteúdo em Buffer. Verifique suporte e estratégia de streaming da versão usada.
Interceptors
Undici oferece mecanismos para compor comportamentos como redirect, retry, cache ou logging conforme a versão da biblioteca. Interceptores devem preservar cancelamento e não repetir operações não idempotentes.
Retry
Faça retry apenas de erros transitórios e métodos seguros ou protegidos por idempotency key. Respeite Retry-After e aplique backoff com jitter.
Redirects
Redirects podem mudar host, protocolo e método. Limite a quantidade e revalide destinos para evitar SSRF e vazamento de Authorization.
Proxy
Ambientes corporativos podem exigir proxy HTTP. Use dispatcher próprio e não confie cegamente em variáveis de ambiente fornecidas por usuários. Proteja credenciais do proxy.
TLS
Configurações de conexão podem incluir CA privada, certificados de cliente e SNI. Não desative rejectUnauthorized. Centralize a configuração por origem.
DNS
O pool mantém conexões ligadas aos IPs resolvidos quando foram abertas. Se o serviço muda de endereço, sockets existentes permanecem. Defina vida útil e reciclagem adequadas.
Consumo do corpo
Um erro comum é verificar o status e lançar exceção sem consumir o corpo:
if (statusCode >= 400) {
const errorBody = await body.text();
throw new Error(`HTTP ${statusCode}: ${errorBody.slice(0, 500)}`);
}Limite o tamanho lido. Respostas de erro podem ser enormes ou maliciosas.
Pool saturado
Quando todas as conexões estão ocupadas, requisições esperam. Uma fila grande aumenta latência. Combine pool com semáforo, deadline, circuit breaker e limite de carga.
Diagnóstico
Monitore:
- conexões abertas;
- conexões ocupadas;
- requisições pendentes;
- tempo na fila;
- tempo de conexão;
- tempo até headers;
- tempo do corpo;
- bytes;
- erros por código;
- resets e timeouts.
Diagnostics Channel
Undici publica eventos de diagnóstico que agentes de observabilidade podem assinar. Use diagnostics_channel com cuidado para não registrar headers sensíveis ou adicionar trabalho pesado.
MockAgent
Testes não precisam acessar a internet:
import {
MockAgent,
setGlobalDispatcher,
} from 'undici';
const mockAgent = new MockAgent();
mockAgent.disableNetConnect();
const mockPool = mockAgent.get('https://api.example.com');
mockPool.intercept({
path: '/users/42',
method: 'GET',
}).reply(200, { id: 42, name: 'Ana' });
setGlobalDispatcher(mockAgent);Restaure o dispatcher após o teste e feche agentes.
Graceful shutdown
await pool.close();close permite concluir o trabalho conforme o contrato. Em prazo excedido, use destruição forçada com consciência de que requisições serão interrompidas.
Erros comuns
- criar Pool por requisição;
- não consumir o corpo;
- usar conexões ilimitadas;
- ativar pipelining sem benchmark;
- não definir deadline;
- repetir POST sem idempotência;
- seguir redirects sem validar;
- registrar Authorization;
- não fechar dispatcher no shutdown;
- confundir erro de rede com status HTTP.
Fluxo recomendado
Use Fetch para casos simples e um Pool ou Agent reutilizável quando precisar de controle. Limite conexões, consuma corpos, propague AbortSignal e monitore fila e timeouts. Combine com HTTP Keep-Alive, AbortController, Circuit Breaker e Diagnostics Channel.
Consulte a documentação oficial do Undici e o repositório oficial do projeto.



