Seu script em Node.js chama a mesma API a cada dez minutos há semanas. Você o leva para um runner de CI, ou um colega o inicia na rede do escritório, e de repente toda chamada termina com uma única linha: TypeError: fetch failed. Sem nome de host, sem código de status. A URL abre no navegador e o curl a alcança da mesma máquina, então a mensagem parece não dizer nada.
Ela diz algo, só que não na mensagem. Este guia mostra onde o Node.js guarda o motivo real, as causas que reproduzimos no Node.js 22, 24 e 26, por que o fetch ignora as variáveis de proxy, a incompatibilidade do undici que quebra agents de proxy no Node.js 26 e um script testado que dá nome à causa.
O que significa "TypeError: fetch failed"?
O fetch() global do Node.js, disponível desde a versão 18, é construído sobre o undici, um cliente HTTP que vem dentro de toda versão do Node.js. O padrão Fetch determina que uma requisição que termina em erro de rede seja rejeitada com um TypeError. O undici usa uma única mensagem fixa para todos esses erros, fetch failed, e anexa o erro real como cause.
Erro de rede é qualquer coisa que dá errado antes de uma resposta: a resolução do nome, a conexão TCP, o handshake TLS, o túnel do proxy ou uma conexão que fecha antes de os cabeçalhos chegarem. Um 404 ou um 500 não é erro de rede; o fetch é resolvido normalmente e res.ok é false.
Quando nada captura o erro, o próprio Node.js imprime a causa. Esta é a saída real de um script de uma linha com um host digitado errado no Node.js 26.10.0:
[TypeError: fetch failed] {
[cause]: Error: getaddrinfo ENOTFOUND api.example-typo.invalid
at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:122:26) {
errno: -3008,
code: 'ENOTFOUND',
syscall: 'getaddrinfo',
hostname: 'api.example-typo.invalid'
}
}O problema começa quando o código captura o erro e registra só a mensagem, como fazem muitos frameworks e SDKs. Imprimir a causa custa uma linha a mais:
try {
await fetch("https://api.example-typo.invalid/v1/items");
} catch (err) {
console.log(err.message); // fetch failed
console.log(err.cause?.code, err.cause?.message); // ENOTFOUND getaddrinfo ENOTFOUND api.example-typo.invalid
}Como encontrar o erro real por trás do fetch failed?
- Registre
err.cause, nãoerr.message. Se um framework engolir a causa, envolva a chamada você mesmo. - Leia
cause.code. Códigos que começam comE(ENOTFOUND,ECONNRESET) vêm do sistema operacional, códigos que começam comUND_ERR_vêm do undici, e códigos comoUNABLE_TO_GET_ISSUER_CERT_LOCALLYvêm da camada TLS. - Verifique se há um
AggregateError. Paralocalhost, o Node.js tenta vários endereços. No nosso teste,cause.messageveio vazio ecause.errorstrazia umECONNREFUSEDpara::1e outro para127.0.0.1. - Desça mais um nível nos erros de proxy. Um login de proxy rejeitado deu
Request was cancelled.sem código; o status estava emerr.cause.cause:Proxy response (407) !== 200 when HTTP Tunneling. - Observe o tempo. Milissegundos indicam uma recusa, uma redefinição ou uma resposta de DNS; cerca de 10 segundos é o tempo limite de conexão; cinco minutos, a espera padrão pelos cabeçalhos.
Códigos de err.cause: o que cada um significa e o que corrigir
Produzimos cada linha no Windows 11 com o Node.js 24.21.0 e 26.10.0, usando um proxy de teste local e servidores locais que recusam, redefinem ou travam conexões, ou apresentam certificados de teste; as duas versões deram os mesmos códigos. A referência de erros do undici documenta os códigos UND_ERR_ e recomenda comparar error.code em vez de usar instanceof, já que o dispatcher pode vir de outra cópia do undici.
cause.code e mensagem | O que aconteceu | O que verificar primeiro |
|---|---|---|
ENOTFOUND getaddrinfo ENOTFOUND host | O nome do host não resolve | Grafia, DNS, VPN |
ECONNREFUSED connect ECONNREFUSED 127.0.0.1:8999 | Nada escuta nessa porta | Serviço, porta, porta do proxy |
ECONNREFUSED dentro de um AggregateError | O mesmo, para cada endereço de localhost | Inicie o servidor |
UND_ERR_CONNECT_TIMEOUT Connect Timeout Error | Nenhuma conexão TCP em 10 s | Endereço, firewall |
ECONNRESET read ECONNRESET | Cortada com uma redefinição TCP | Proxy, firewall; nova tentativa |
UND_ERR_SOCKET other side closed | Fechada antes de qualquer resposta | Logs do servidor; nova tentativa |
UND_ERR_HEADERS_TIMEOUT Headers Timeout Error | Conectou, mas sem cabeçalhos a tempo | Seu limite de tempo |
UNABLE_TO_GET_ISSUER_CERT_LOCALLY, SELF_SIGNED_CERT_IN_CHAIN | Certificado raiz não confiável | Certificado raiz da empresa |
UNABLE_TO_VERIFY_LEAF_SIGNATURE | O servidor não enviou o intermediário | Cadeia de certificados do servidor |
ERR_SSL_WRONG_VERSION_NUMBER | TLS enviado a uma porta HTTP sem criptografia | https:// na URL do proxy |
UND_ERR_INVALID_ARG invalid onError method | Agent de outra versão principal do undici | O fetch do próprio undici |
| Sem código: Request was cancelled. | O proxy recusou o túnel | Credenciais do proxy |
Há uma mensagem relacionada que não é fetch failed: se os cabeçalhos chegam mas o corpo trava, res.text() ou res.json() lança TypeError: terminated, no nosso teste com UND_ERR_BODY_TIMEOUT como causa.
ENOTFOUND e ECONNREFUSED: a requisição nunca chegou a um servidor
ENOTFOUND vem da resolução de nomes: o resolvedor disse que o nome não existe. Procure um erro de digitação, uma VPN com servidores DNS próprios ou um contêiner que não alcança o DNS interno da sua empresa. Atrás de um proxy, quem resolve o nome é o proxy, então um host errado volta como código de status do proxy: nosso proxy de teste respondeu 502, informado como Proxy response (502) !== 200 when HTTP Tunneling.
ECONNREFUSED significa que a máquina respondeu, mas nada aceita conexões naquela porta. Se o endereço na mensagem é o do seu proxy, o que está errado é a configuração do proxy, não o destino. Com localhost, o Node.js tenta tanto ::1 quanto 127.0.0.1; no nosso teste, um servidor vinculado só a 127.0.0.1 ainda respondeu a http://localhost. Quando nenhum dos dois responde, o servidor de desenvolvimento está fora do ar ou usa outra porta, ou o seu código roda no Docker, onde localhost é o próprio contêiner.
ECONNRESET, "other side closed" e "socket hang up"
Os três significam que uma conexão foi aberta e depois se rompeu. Comparamos o fetch com o módulo http e com o Axios 1.20:
- Um servidor que respondeu com uma redefinição TCP produziu
read ECONNRESETnos três. - Um servidor que fechou a conexão sem responder produziu
UND_ERR_SOCKET(other side closed) no fetch, esocket hang upcom o códigoECONNRESETnohttpe no Axios.
Motivos típicos: o servidor caiu no meio da requisição, um balanceador de carga ou proxy fechou uma conexão keep-alive ociosa bem quando o seu cliente a reutilizava, ou um firewall derrubou uma conexão longa. Alguns servidores e filtros de bots também fecham conexões que não querem atender; isso significa ir mais devagar ou pedir acesso, não insistir com mais tentativas.
Repita requisições idempotentes (GET, HEAD) uma ou duas vezes com uma espera crescente. Não repita às cegas um POST que cria um pedido: o servidor pode ter feito o trabalho antes de a conexão cair.
Tempos limite: UND_ERR_CONNECT_TIMEOUT, UND_ERR_HEADERS_TIMEOUT e AbortSignal.timeout()
O fetch no Node.js não tem um limite de tempo total. O undici desiste de abrir uma conexão TCP depois de 10 segundos, depois espera até 300 segundos pelos cabeçalhos e até 300 segundos entre partes do corpo. Um servidor que aceita a conexão e trava pode segurar uma requisição por cinco minutos. Defina o seu próprio limite:
try {
const res = await fetch("https://api.example.com/v1/items", { signal: AbortSignal.timeout(5_000) });
console.log(res.status);
} catch (err) {
if (err.name === "TimeoutError") console.log("gave up after 5 s");
else console.log(err.message, err.cause?.code);
}Contra um servidor local que nunca responde, o código imprimiu gave up after 5 s no Node.js 24 e 26. Como explica a página da MDN sobre AbortSignal.timeout(), o sinal aborta com um TimeoutError, não com fetch failed, então verifique err.name. O limite também cobre o corpo: um corpo travado fez res.text() lançar o mesmo TimeoutError. Mudar os limites do próprio undici exige um Agent do pacote do npm, o que traz a questão de versões que vemos mais abaixo.
Por que o fetch ignora HTTP_PROXY e HTTPS_PROXY?
O curl, o pip e muitas outras ferramentas leem as variáveis de ambiente de proxy; o fetch do Node.js não. Com HTTPS_PROXY definida, o fetch no Node.js 22.23.3, 24.21.0 e 26.10.0 foi direto ao destino e o log do nosso proxy ficou vazio, enquanto o curl usou o proxy. Onde só o proxy chega à internet, a causa então aponta o destino (ENOTFOUND, UND_ERR_CONNECT_TIMEOUT), nunca o proxy que foi ignorado.
O suporte nativo a proxy do Node.js liga esse comportamento: NODE_USE_ENV_PROXY=1 (Node.js 22.21.0 e 24.0.0 ou posteriores) ou a flag --use-env-proxy (22.21.0 e 24.5.0 ou posteriores). O Node.js então lê HTTP_PROXY, HTTPS_PROXY e NO_PROXY na inicialização, para o fetch e para os módulos http e https. A documentação ainda marca o recurso como em desenvolvimento ativo.
NODE_USE_ENV_PROXY=1 HTTPS_PROXY="http://user:pass@pr.proxynet.io:8000" NO_PROXY="localhost,127.0.0.1" node app.mjsNo PowerShell:
$env:NODE_USE_ENV_PROXY = "1"
$env:HTTPS_PROXY = "http://user:pass@pr.proxynet.io:8000"
node app.mjsNosso proxy de teste então registrou CONNECT example.com:443 nas três versões; o Node.js 22 também avisou que o EnvHttpProxyAgent é experimental. Três detalhes costumam confundir:
- A URL do proxy começa com
http://, mesmo para destinoshttps://. A requisição do túnel é HTTP sem criptografia e o TLS roda dentro dela;https://na frente de um proxy HTTP sem criptografia deuERR_SSL_WRONG_VERSION_NUMBER. http.setGlobalProxyFromEnv()faz o mesmo pelo código, a partir do Node.js 24.14.0 e 25.4.0; o Node.js 22 não tem essa função.- Uma senha errada se esconde um nível mais abaixo, em
err.cause.cause.
O gateway de um provedor, como o dos nossos Proxies residenciais, entra na mesma variável como http://user:pass@pr.proxynet.io:8000, ou sem credenciais quando o IP do seu servidor já estiver na whitelist de IP.
Para proxy por requisição, Axios e SOCKS5, siga o nosso guia de como usar proxy no Node.js.
Node.js 26 e "invalid onError method": a incompatibilidade de versões do undici
Cada versão do Node.js traz o próprio undici, separado do pacote undici do npm: o Node.js 22.23.3 inclui o undici 6.28.1, o 24.21.0 inclui o 7.29.1 e o 26.10.0 inclui o 8.10.2. O undici 8.0.0 removeu os wrappers que permitiam que código de handler mais antigo conversasse com código mais novo. Segundo o calendário de versões do Node.js, o Node.js 26 passa a ser a linha Active LTS em 28 de outubro de 2026, então muitos projetos vão topar com isso na atualização.
O erro aparece quando um ProxyAgent ou Agent do pacote do npm é passado como dispatcher ao fetch global. Testamos quatro versões principais do npm em três versões do Node.js:
| npm undici | Node.js 22.23.3 | Node.js 24.21.0 | Node.js 26.10.0 |
|---|---|---|---|
| 5.29.0 | funciona | funciona | invalid onError method |
| 6.29.0 | funciona | funciona | invalid onError method |
| 7.30.0 | funciona | funciona | funciona |
| 8.11.2 | invalid onRequestStart method | invalid onRequestStart method | funciona |
Cada falha foi TypeError: fetch failed com UND_ERR_INVALID_ARG como causa. Uma variante mais silenciosa é pior: no Node.js 26, setGlobalDispatcher(new ProxyAgent(...)) do undici 5 ou 6 não gerou erro nenhum, o fetch o ignorou e a requisição saiu direto, enquanto nosso proxy não via nada.
A correção é pegar o fetch do mesmo pacote que o agent:
import { fetch, ProxyAgent } from "undici"; // fetch e o agent do mesmo pacote
const proxy = new ProxyAgent("http://user:pass@pr.proxynet.io:8000");
const res = await fetch("https://example.com/", { dispatcher: proxy });
console.log(res.status); // 200 no nosso teste, por um proxy de teste localIsso funcionou com o undici 8.11.2 no Node.js 24 e 26. Se a incompatibilidade estiver em uma ferramenta que você não escreveu, como uma CLI que embute um undici antigo e cria um agent a partir das suas variáveis de proxy, atualize a ferramenta ou mantenha-a no Node.js 24 até sair a correção.
Erros de certificado por trás do fetch failed
Uma verificação TLS que falha também chega como fetch failed, com o código do certificado em cause.code. No trabalho, a origem comum é um proxy corporativo ou antivírus que inspeciona HTTPS e assina o tráfego de novo com o próprio certificado raiz. O Node.js verifica contra a própria lista embutida de autoridades raiz, não contra a do sistema operacional, então rejeita essa raiz com UNABLE_TO_GET_ISSUER_CERT_LOCALLY ou SELF_SIGNED_CERT_IN_CHAIN.
Aponte NODE_EXTRA_CA_CERTS para um arquivo PEM com o certificado raiz da empresa, ou rode o Node.js com --use-system-ca (a partir do 22.15.0 e do 23.8.0) se a raiz estiver instalada na máquina. No nosso teste, NODE_EXTRA_CA_CERTS resolveu o primeiro caso, mas não UNABLE_TO_VERIFY_LEAF_SIGNATURE, em que o servidor deixa de enviar o certificado intermediário e só o dono dele pode corrigir. As correções para npm, Git, Python e curl estão em Unable to get local issuer certificate.
Um wrapper de fetch que dá nome à causa e tenta de novo
O script usa o fetch global, então não precisa de pacotes. Ele percorre toda a cadeia de causas, limita cada tentativa com AbortSignal.timeout() e só tenta de novo em redefinições, sockets fechados e tempos limite esgotados, com backoff exponencial mais jitter. Falhas de DNS, recusas, erros de certificado e um login de proxy rejeitado falham do mesmo jeito toda vez, então ele para neles. Em 429 e 503, ele espera o tempo indicado em Retry-After.
Um 429 é o servidor pedindo que você vá mais devagar; o que o cabeçalho significa e como controlar o ritmo de um trabalho está em HTTP 429 Too Many Requests.
// fetch-check.mjs: dá nome à causa real por trás de "TypeError: fetch failed" e só tenta de novo o que pode se recuperar.
// Testado no Node.js 22, 24 e 26. Proxy opcional: NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://user:pass@pr.proxynet.io:8000
const RETRY_CODES = new Set(["ECONNRESET", "UND_ERR_SOCKET", "UND_ERR_CONNECT_TIMEOUT"]);
const CERT_CODES = /CERT|SELF_SIGNED|UNABLE_TO_(GET|VERIFY)/;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/** O erro e cada erro embrulhado dentro dele, do mais externo para o mais interno. */
function causes(err) {
const chain = [];
for (let e = err; e && chain.length < 8; e = e.cause) {
chain.push(e);
if (e instanceof AggregateError) chain.push(...e.errors); // "localhost": um erro por endereço
}
return chain;
}
/** O primeiro código de erro em string na cadeia, como "ECONNRESET". */
const codeOf = (err) => causes(err).map((e) => e.code).find((c) => typeof c === "string") ?? "";
/** Uma linha: o que falhou e o que verificar primeiro. */
function explain(err) {
if (err.name === "TimeoutError") return "no answer before our AbortSignal.timeout(): slow server or proxy";
const chain = causes(err);
const text = chain.map((e) => e.message).join(" | ");
const code = codeOf(err);
const tunnel = text.match(/Proxy response \((\d{3})\)/);
if (tunnel?.[1] === "407") return "proxy wants credentials (407): check user:pass or the IP whitelist";
if (tunnel) return `proxy refused the tunnel (${tunnel[1]}): the proxy could not or would not reach the target`;
if (code === "ENOTFOUND") return `name ${chain.find((e) => e.hostname)?.hostname} does not resolve: typo, DNS or VPN`;
if (code === "ECONNREFUSED") return "nothing listens on that address and port: wrong port, service down or wrong proxy port";
if (code === "ECONNRESET") return "the connection was cut (TCP reset): server, proxy or firewall dropped it";
if (code === "UND_ERR_SOCKET") return "the other side closed the connection before answering";
if (code === "UND_ERR_CONNECT_TIMEOUT") return "no TCP connection within 10 s: wrong IP, firewall or blocked outbound port";
if (code === "UND_ERR_HEADERS_TIMEOUT") return "connected, but no response headers in time";
if (code === "ERR_SSL_WRONG_VERSION_NUMBER") return "TLS spoken to a plain-HTTP port: the proxy URL should start with http://";
if (CERT_CODES.test(code)) return `certificate not trusted (${code}): add your CA with NODE_EXTRA_CA_CERTS`;
return `unrecognised, read the innermost error: ${chain.at(-1).message}`;
}
/** GET com limite de tempo rígido, backoff para falhas de rede e Retry-After para 429/503. */
async function getWithRetry(url, { attempts = 3, timeoutMs = 15_000 } = {}) {
for (let attempt = 1; ; attempt++) {
const backoff = 500 * 2 ** (attempt - 1) + Math.random() * 250; // 0,5 s, 1 s, 2 s ... mais jitter
let res;
try {
res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
} catch (err) {
const code = codeOf(err);
const retryable = RETRY_CODES.has(code) || err.name === "TimeoutError";
if (!retryable || attempt === attempts) throw err;
console.log(` attempt ${attempt}: ${code || err.name}, retrying in ${Math.round(backoff)} ms`);
await sleep(backoff);
continue;
}
if ((res.status === 429 || res.status === 503) && attempt < attempts) {
const wait = Number(res.headers.get("retry-after")) * 1000 || backoff; // só o formato em segundos
await res.body?.cancel();
console.log(` attempt ${attempt}: HTTP ${res.status}, waiting ${Math.round(wait)} ms`);
await sleep(wait);
continue;
}
return res;
}
}
for (const url of process.argv.slice(2)) {
console.log(url);
try {
const res = await getWithRetry(url, { timeoutMs: 5_000 });
console.log(` OK: HTTP ${res.status}`);
} catch (err) {
console.log(` FAILED: ${err.name}: ${err.message} -> ${explain(err)}`);
}
}Como fica a saída
Rodamos o script no Node.js 26.10.0 contra um host digitado errado, uma porta fechada em localhost e servidores locais que redefinem toda conexão (porta 8902), nunca respondem (8901), usam um certificado da nossa própria autoridade de teste (8906) e respondem 429 duas vezes antes de a requisição dar certo (8910):
https://api.example-typo.invalid/
FAILED: TypeError: fetch failed -> name api.example-typo.invalid does not resolve: typo, DNS or VPN
http://localhost:8999/
FAILED: TypeError: fetch failed -> nothing listens on that address and port: wrong port, service down or wrong proxy port
http://127.0.0.1:8902/
attempt 1: ECONNRESET, retrying in 652 ms
attempt 2: ECONNRESET, retrying in 1115 ms
FAILED: TypeError: fetch failed -> the connection was cut (TCP reset): server, proxy or firewall dropped it
http://127.0.0.1:8901/
attempt 1: TimeoutError, retrying in 658 ms
attempt 2: TimeoutError, retrying in 1024 ms
FAILED: TimeoutError: The operation was aborted due to timeout -> no answer before our AbortSignal.timeout(): slow server or proxy
https://127.0.0.1:8906/
FAILED: TypeError: fetch failed -> certificate not trusted (UNABLE_TO_GET_ISSUER_CERT_LOCALLY): add your CA with NODE_EXTRA_CA_CERTS
http://127.0.0.1:8910/items
attempt 1: HTTP 429, waiting 1000 ms
attempt 2: HTTP 429, waiting 1000 ms
OK: HTTP 200Depois, pelo proxy de teste local com NODE_USE_ENV_PROXY=1: a senha certa, uma errada e https:// na frente do endereço do proxy:
https://example.com/
OK: HTTP 200
https://example.com/
FAILED: TypeError: fetch failed -> proxy wants credentials (407): check user:pass or the IP whitelist
https://example.com/
FAILED: TypeError: fetch failed -> TLS spoken to a plain-HTTP port: the proxy URL should start with http://O Node.js 22.23.3 e o 24.21.0 imprimiram as mesmas linhas, tirando os valores aleatórios de backoff.
Onde você encontra esse erro
- Next.js e outros frameworks do lado do servidor, cujas páginas de erro muitas vezes mostram só a mensagem.
- Servidores MCP e ferramentas de agentes de IA, que muitas vezes rodam atrás de uma VPN ou de um proxy corporativo.
- SDKs construídos sobre o fetch, alguns dos quais descartavam a causa em versões antigas.
- Runners de CI e builds do Docker que não têm as variáveis de proxy nem o certificado raiz da empresa que o host tem.
- Trabalhos de scraping e de monitoramento, que em execuções longas esbarram em quase toda a tabela.
Trabalhos longos de coleta de dados são os que mais ganham com o wrapper. Se um trabalho assim lida com poucos destinos conhecidos e precisa de IPs de saída que fiquem iguais por semanas, os Proxies de datacenter dão a você endereços IPv4 dedicados com tráfego sem cota.
Erros comuns
- Registrar
err.message. Ela sempre dizfetch failed. - Esperar que o fetch leia
HTTPS_PROXY. Ele não lê semNODE_USE_ENV_PROXY=1ou--use-env-proxy. - Passar um agent do undici do npm para o fetch global. Importe o
fetchdo mesmo pacote. - Desligar a verificação de certificados.
NODE_TLS_REJECT_UNAUTHORIZED=0desativa a verificação para o processo inteiro, então um atacante no caminho fica igual ao seu proxy corporativo. - Tentar de novo em tudo. Um erro de digitação falha do mesmo jeito toda vez; um POST depois de
ECONNRESETpode rodar duas vezes. - Não definir limite de tempo. Um servidor travado pode segurar uma requisição por cinco minutos.
Guia de decisão
O que você vê em err.cause | O que fazer |
|---|---|
ENOTFOUND | Corrija o nome do host ou o DNS; sem nova tentativa |
ECONNREFUSED com o endereço do seu proxy | Copie de novo o host e a porta do proxy |
ECONNREFUSED em localhost | Inicie o servidor; verifique a porta e o Docker |
ECONNRESET, UND_ERR_SOCKET | Repita requisições GET com backoff |
UND_ERR_CONNECT_TIMEOUT | Verifique o firewall; atrás de um proxy corporativo, defina NODE_USE_ENV_PROXY=1 |
Espera longa e depois UND_ERR_HEADERS_TIMEOUT | Adicione AbortSignal.timeout() |
| Um código de certificado | NODE_EXTRA_CA_CERTS ou --use-system-ca |
Proxy response (407) um nível abaixo | Verifique user:pass ou a whitelist de IP |
invalid onError method | Importe o fetch do pacote undici do agent |
Perguntas frequentes
Por que o fetch não lança erro em um 404 ou 500?
Porque o servidor respondeu. O fetch só rejeita em erros de rede; um erro HTTP é resolvido com res.ok igual a false, então verifique isso antes de ler o corpo.
"fetch failed" é o mesmo que "Failed to fetch" no navegador?
São parecidos, mas não iguais. O Chrome rejeita com TypeError: Failed to fetch e não diz à página o porquê, então um bloqueio de CORS parece idêntico; abra a guia Rede no DevTools. O Node.js coloca o motivo em cause.
Como definir um tempo limite para o fetch no Node.js?
Passe signal: AbortSignal.timeout(ms). Quando ele dispara, o fetch rejeita com um TimeoutError, e o limite também interrompe um corpo travado.
O fetch do Node.js usa HTTP_PROXY e HTTPS_PROXY?
Só com NODE_USE_ENV_PROXY=1 (22.21.0, 24.0.0 e posteriores) ou --use-env-proxy (22.21.0, 24.5.0 e posteriores). Caso contrário, as variáveis são ignoradas em silêncio.
Por que o curl funciona quando o fetch falha na mesma máquina?
Normalmente por um de dois motivos: o curl lê as variáveis de proxy e o fetch não, e muitas builds do curl, incluindo a do Windows, usam o repositório de certificados do sistema, enquanto o Node.js usa a própria lista, a menos que você passe --use-system-ca.
Um proxy pode resolver "TypeError: fetch failed"?
Só quando o caminho de rede é a causa, como em uma rede corporativa em que só o proxy chega à internet. Ele não corrige um erro de digitação, um problema de certificado ou um servidor fora do ar, e não é um jeito de contornar um site que derruba sua conexão ou aplica limite de taxa: vá mais devagar, use a API oficial ou peça acesso.
Em resumo
TypeError: fetch failed é só um invólucro: o motivo está em err.cause, às vezes um nível mais abaixo. Leia o código, corrija o que ele aponta, dê a cada chamada um AbortSignal.timeout() e só tente de novo em redefinições e tempos limite esgotados. Atrás de um proxy, defina NODE_USE_ENV_PROXY=1, mantenha http:// na URL do proxy e pegue o fetch e o agent do mesmo pacote undici, principalmente no Node.js 26. Quando o seu código estiver pronto para um proxy, compare as opções na nossa página de serviços de proxy.




