O nosso problema reside numa armadilha específica: somos a Geonode e vendemos proxies, e o fetch integrado no Node ignora completamente as variáveis de ambiente HTTP_PROXY e HTTPS_PROXY. Todos os outros clientes HTTP do ecossistema respeitam-nas, pelo que as pessoas configuram um proxy, vêem que os pedidos são bem-sucedidos e assumem que está a funcionar — enquanto o tráfego segue diretamente. Não há qualquer aviso nem erro. A correção consiste em algumas linhas e encontra-se na secção sobre proxies abaixo. Se estiver a utilizar um proxy para o tráfego do Node fetch e não tiver definido explicitamente um dispatcher, é quase certo que as suas solicitações não estão a passar pelo seu proxy.
Utilizar o fetch nativo ou o pacote node-fetch?
Comece por aqui, pois isto determina o que irá instalar.
A documentação do Node's indica que o fetch foi adicionado nas versões v17.5.0 e v16.15.0, deixou de estar sujeito ao sinalizador --experimental-fetch a partir da versão v18.0.0 e «já não é experimental» a partir da versão v21.0.0. É descrito como «uma implementação compatível com navegadores da função fetch(), baseada no undici, um cliente HTTP/1.1 escrito de raiz para o Node.js». Headers, Request e Response seguem a mesma cronologia.
Assim, em qualquer versão do Node atualmente suportada, fetch é uma variável global e não é necessária qualquer dependência.
O pacote node-fetch continua a ser útil em dois casos: na manutenção de código num ambiente de execução mais antigo e quando é necessário recorrer a um dos poucos comportamentos em que a sua API difere. Note-se que a versão 3 é exclusivamente ESM, o que cria dificuldades a projetos que ainda utilizam require.
Tudo o que se segue aplica-se a ambos, uma vez que a API é deliberadamente a mesma.
As três formas de definir cabeçalhos
Um objeto simples — o caso mais comum e aquele a utilizar na maioria das vezes:
const res = await fetch("https://api.example.com/items", {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer eyJhbG...",
"Accept": "application/json",
},
});
**Um objeto ``Headers`
`** — quando se constrói a configuração de forma condicional:
const headers = new Headers({ "Accept": "application/json" });
if (token) headers.set("Authorization", `Bearer ${token}`);
if (locale) headers.set("Accept-Language", locale);
const res = await fetch(url, { headers });
Um array de pares — útil quando um cabeçalho se repete legitimamente:
const res = await fetch(url, {
headers: [
["Accept", "application/json"],
["X-Trace", "a"],
["X-Trace", "b"],
],
});
Todas as três formas são equivalentes no caso simples. O objeto ``Headers`
` justifica a sua utilização quando é necessária lógica condicional ou quando se pretende inspecionar o que foi construído antes do envio.
set
versus append
: a distinção que causa surpresas.
Os documentos da MDN definem set()
como a definição de «um novo valor para um cabeçalho existente» e a substituição dos valores existentes, enquanto append()
«acrescenta um novo valor a um cabeçalho existente ou adiciona-o caso não exista».
const h = new Headers();
h.append("X-Custom", "one");
h.append("X-Custom", "two");
h.get("X-Custom"); // "one, two"
h.set("X-Custom", "three");
h.get("X-Custom"); // "three"
append
acumula; set
substitui. Para quase todos os cabeçalhos que enviar, set
é o que pretende — enviar dois valores Authorization
não é um pedido significativo. append
é relevante para cabeçalhos em que vários valores são legítimos e, na prática, essa lista é curta.
Os nomes dos cabeçalhos não distinguem maiúsculas de minúsculas. A MDN refere que são «comparados com base numa sequência de bytes que não distingue maiúsculas de minúsculas» em todos os métodos, pelo que h.get("content-type")
e h.get("Content-Type")
devolvem o mesmo valor. Escolha uma convenção para facilitar a leitura e deixe de se preocupar com isso.
Existe também has()
para testar a presença, delete()
para remover e getSetCookie()
, que devolve um array com todos os valores de Set-Cookie
— necessário porque esse cabeçalho é o principal caso em que vários valores coexistem genuinamente e um simples get()
os concatenaria num resultado que não seria possível dividir de forma fiável.
Cabeçalhos que não é possível definir
A razão pela qual um cabeçalho que configurou não aparece.
A MDN descreve uma «proteção» nos objetos Headers que determina o que pode ser modificado. Um new Headers() independente não tem restrições. Os cabeçalhos associados a um Request permitem a modificação de «cabeçalhos de pedido não proibidos». E os cabeçalhos num Response obtidos «a partir de Response.error(), Response.redirect() ou fetch()» são imutáveis — não é possível alterar os cabeçalhos de uma resposta após a sua receção.
Os cabeçalhos de pedido proibidos são aqueles controlados pelo ambiente de execução, e as tentativas de os definir são ignoradas silenciosamente, em vez de gerarem um erro. A lista inclui Host, Connection, Content-Length, Transfer-Encoding, Origin, Referer em alguns contextos, e as famílias com os prefixos Sec- e Proxy-.
Duas consequências práticas.
O silêncio é o modo de falha. Sem exceção, sem aviso; o cabeçalho simplesmente não é enviado. Se um servidor insistir que não está a receber algo que definiu, verifique o que realmente foi transmitido em vez de reler o seu código.
O Node é mais permissivo do que um navegador em alguns destes casos, porque não há nenhuma origem a proteger. Código que define um cabeçalho com sucesso no Node pode ver esse cabeçalho ser ignorado num navegador, o que constitui uma verdadeira armadilha de portabilidade para código partilhado.
Para verificar o que enviou efetivamente, envie um pedido a um serviço que o devolva:
const res = await fetch("https://httpbin.org/headers", {
headers: { "X-Test": "value", "User-Agent": "MyBot/1.0" },
});
console.log(await res.json());
Leitura de cabeçalhos de resposta
A outra parte, e há um comportamento que vale a pena conhecer.
const res = await fetch(url);
res.headers.get("content-type");
res.headers.has("etag");
for (const [name, value] of res.headers) {
console.log(name, value);
}
A iteração produz nomes em minúsculas, uma vez que o conjunto de cabeçalhos é normalizado.
**Set-Cookie
requer um tratamento especial.** Vários cookies chegam como vários cabeçalhos, e um simples get("set-cookie")
apresenta-os separados por vírgulas — o que é ambíguo, porque os próprios valores dos cookies podem conter vírgulas numa data Expires
. getSetCookie()
existe precisamente para isso e devolve um array:
const cookies = res.headers.getSetCookie();
Os cabeçalhos de resposta são imutáveis. Não é possível modificar o que fetch
devolveu. Se precisar de uma versão alterada, construa um novo Response
.
E a verificação que importa mais do que qualquer cabeçalho: fetch
não rejeita em caso de códigos de erro HTTP. Um 404 ou um 500 são resolvidos normalmente, pelo que res.ok
deve ser testado antes de interpretar o corpo.
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
Ignorar isso é a causa direta de Unexpected token '<'
numa chamada a .json()
— analisou uma página de erro.
Cabeçalhos predefinidos e o que o Node adiciona
O Node define vários cabeçalhos automaticamente, e saber quais são evita confusões.
Host — derivado do URL, não configurável.
Connection — gerido pelo conjunto de ligações.
Content-Length — calculado a partir do corpo da resposta.
Accept — o valor predefinido é */*, a menos que o defina.
Accept-Encoding — o Node indica que suporta compressão e descomprime a resposta de forma transparente.
User-Agent — o Node envia o seu próprio por predefinição, identificando normalmente o undici.
Este último aspeto é importante para qualquer interação com terceiros. Um agente de utilizador de tempo de execução predefinido é uma identificação precisa e, para clientes automatizados, uma identificação fraca — um nome honesto com um URL de contacto é tratado melhor do que uma cadeia de caracteres anónima de tempo de execução:
headers: { "User-Agent": "AcmeBot/1.0 (+https://acme.example.com/bot)" }
Uma nota sobre os corpos: quando passar um objeto FormData, não defina Content-Type manualmente. O ambiente de execução deve gerá-lo, porque contém o limite multipart, e substituí-lo produz uma solicitação que o servidor não consegue analisar. Esta é uma das causas mais comuns de um erro 400 ou 415 inexplicável.
Definir cabeçalhos para cada pedido
Para tudo o que não seja um script, centralize-o.
const DEFAULTS = {
"Accept": "application/json",
"User-Agent": "AcmeBot/1.0 (+https://acme.example.com/bot)",
};
async function api(path, options = {}) {
const res = await fetch(`https://api.example.com${path}`, {
...options,
headers: { ...DEFAULTS, ...options.headers },
});
if (!res.ok) {
const body = await res.text();
throw new Error(`HTTP ${res.status} ${path}: ${body.slice(0, 200)}`);
}
return res;
}
Há dois pormenores aqui que merecem destaque. Definir os valores por defeito primeiro significa que quem faz a chamada pode substituir qualquer um deles, o que é precisamente o comportamento pretendido. E incluir os primeiros 200 caracteres do corpo do erro transforma um código de estado opaco numa mensagem com base na qual se pode agir.
Note que a propagação do objeto é superficial e corresponde à sequência exata da chave; por isso, "content-type" nas opções do chamador não substituirá "Content-Type" nos valores predefinidos — enviará ambos, e o tempo de execução escolherá um. Se os chamadores puderem usar maiúsculas e minúsculas arbitrárias, crie um objeto Headers e deixe que o seu set(), insensível a maiúsculas e minúsculas, trate da fusão corretamente.
Depuração de cabeçalhos que não estão a funcionar
Uma sequência que resolve praticamente todos os problemas de cabeçalhos em poucos minutos, na ordem que elimina o maior número de possibilidades.
Primeiro — verifique o que foi realmente enviado pela rede. Nada mais nesta lista importa até que tenha feito isto. Um serviço de eco de cabeçalhos é a forma mais rápida:
const res = await fetch("https://httpbin.org/headers", { headers: myHeaders });
console.log(JSON.stringify(await res.json(), null, 2));
Se o seu cabeçalho não estiver aqui, significa que nunca saiu do seu processo — está proibido, está mal escrito ou foi sobrescrito. Se estiver aqui e o destino indicar o contrário, algo entre si e o destino está a removê-lo.
Segundo — crie o objeto Headers e inspecione-o antes de enviar. Isto distingue «construí-o incorretamente» de «o ambiente de execução descartou-o»:
const h = new Headers(myHeaders);
console.log([...h.entries()]);
A construção de um objeto Headers aplica a mesma normalização que o ambiente de execução aplicaria, pelo que um nome que sobreviva aqui é um nome que será enviado.
Três — verifique se há duplicações acidentais. A armadilha da fusão superficial: uma expansão de objeto compara chaves por cadeia de caracteres exata, pelo que {...{"Content-Type": "a"}, ...{"content-type": "b"}} produz ambas as entradas. Crie um objeto Headers e utilize set() se os chamadores puderem fornecer maiúsculas e minúsculas arbitrárias, uma vez que a sua correspondência insensível a maiúsculas e minúsculas realiza a fusão corretamente.
Quatro — reproduza-o no curl. Se a mesma solicitação funcionar a partir de um terminal e não a partir do Node, a diferença está no seu código e não no servidor:
curl -v -H "Authorization: Bearer $TOKEN" https://api.example.com/items 2>&1 | grep '^>'
Comparar os dois blocos > lado a lado geralmente torna a diferença óbvia.
Cinco — leia a resposta na íntegra, não apenas o código de estado. Um código de estado 400 ou 401 inclui frequentemente um corpo que explica exatamente qual o cabeçalho que estava errado, e o código que o descarta está a desperdiçar a resposta:
if (!res.ok) console.error(res.status, (await res.text()).slice(0, 300));
E verifique o redirecionamento. O fetch segue redirecionamentos por predefinição, e alguns cabeçalhos — em particular o Authorization — são descartados quando um redirecionamento passa para uma origem diferente. Se uma solicitação funcionar diretamente na URL final, mas não na original, essa é quase certamente a razão.
A armadilha do proxy
O comportamento que difere de todos os outros clientes HTTP do Node e a razão pela qual esta secção existe.
**O ``fetch`
do Node não lê ``HTTP_PROXY
, ``HTTPS_PROXY
ou ``NO_PROXY
`.** Definir esses valores não altera nada. Os pedidos são enviados diretamente, são bem-sucedidos e nada indica que o proxy foi contornado.
A solução é o ``ProxyAgent`
` do undici:
import { ProxyAgent, setGlobalDispatcher } from "undici";
setGlobalDispatcher(new ProxyAgent("http://user:pass@proxy.example.com:9000"));
// now every fetch in this process goes through the proxy
const res = await fetch("https://api.example.com/items");
Para uma única solicitação, em vez de todo o processo, passe um dispatcher por chamada:
const agent = new ProxyAgent("http://proxy.example.com:9000");
const res = await fetch(url, { dispatcher: agent });
Note que ``dispatcher`
` é uma extensão específica do Node, e não faz parte da API Fetch padrão; por isso, o código que a utiliza não é portável para um navegador.
Verifique sempre se a alteração surtiu efeito. Pergunte a um serviço qual o endereço que este vê, com e sem o despachante:
const res = await fetch("https://api.ipify.org?format=json");
console.log(await res.json());
Se o endereço não mudar, o proxy não está no caminho — e, dado que não há nenhum erro a alertá-lo, esta verificação é a única coisa que separa uma configuração funcional de uma que é silenciosamente contornada. Trata-se do mesmo tipo de falha silenciosa sobre a qual escrevemos em por que é importante testar proxies.
As diferenças de comportamento entre os clientes HTTP neste contexto são exatamente o tipo de coisa que comparámos em axios vs fetch.
Perguntas frequentes
Como configuro os cabeçalhos com o node-fetch?
Passe um objeto headers nas opções: fetch(url, { headers: { "Authorization": "Bearer ..." } }). Também pode passar uma instância de Headers ou um array de pares nome-valor. A mesma sintaxe funciona com o método fetch integrado do Node.
Ainda preciso do pacote node-fetch?
Normalmente, não. O Node tem uma variável global fetch desde a versão 17.5.0, sem marcação desde a versão 18 e estável desde a versão 21. Instale o pacote apenas para ambientes de execução mais antigos ou para uma diferença de comportamento específica — e tenha em atenção que a versão 3 é apenas para ESM.
Qual é a diferença entre headers.set e headers.append?
set substitui qualquer valor existente para esse cabeçalho; append adiciona outro valor, pelo que duas chamadas a append produzem uma lista separada por vírgulas. Utilize set para quase tudo — append só é relevante para cabeçalhos em que vários valores são válidos.
Porque é que o meu cabeçalho não está a ser enviado?
Provavelmente trata-se de um cabeçalho proibido que o ambiente de execução controla — Host, Connection, Content-Length e a família Sec-, entre outros. Estes são ignorados silenciosamente, em vez de gerarem um erro. Envie um pedido a um serviço de eco de cabeçalhos para ver o que realmente foi transmitido.
Os nomes dos cabeçalhos distinguem maiúsculas de minúsculas no fetch?
Não. O MDN especifica que os nomes dos cabeçalhos são comparados com base numa sequência de bytes que não distingue maiúsculas de minúsculas em todos os métodos Headers, pelo que get("content-type") e get("Content-Type") são equivalentes. A iteração de um objeto Headers produz nomes em minúsculas.
Como posso ler vários cabeçalhos Set-Cookie?
Utilize res.headers.getSetCookie(), que devolve um array. Um simples get("set-cookie") junta-os com vírgulas, o que é ambíguo porque os próprios valores dos cookies podem conter vírgulas numa data Expires.
Por que é que o Node fetch ignora a minha configuração HTTP_PROXY?
Porque não lê essas variáveis de ambiente de todo, ao contrário de quase todos os outros clientes HTTP do Node. Utilize o ProxyAgent do undici com setGlobalDispatcher, ou passe um dispatcher por pedido — e, em seguida, verifique o endereço de saída, uma vez que um proxy contornado não produz qualquer erro.
Devo definir o Content-Type ao enviar FormData?
Não. O tempo de execução gera-o incluindo o delimitador multipart, e defini-lo manualmente remove esse delimitador, produzindo uma solicitação que o servidor não consegue analisar. Esta é uma causa comum de respostas 400 e 415 inexplicáveis.
Conclusão
Definir cabeçalhos no Node é uma questão de uma única linha, independentemente da API que utilizar, e o fetch integrado significa que a maioria dos projetos já nem sequer precisa de um pacote para o efeito.
Três comportamentos são responsáveis por quase toda a confusão. set substitui, enquanto append acumula, e inverter essa ordem produz valores de cabeçalho separados por vírgulas que os servidores rejeitam. Os cabeçalhos proibidos são ignorados silenciosamente, em vez de gerarem um erro; por isso, um cabeçalho que o servidor não esteja a receber tem de ser verificado na transmissão, em vez de ser relido no seu editor. E fetch resolve-se em erros HTTP, pelo que res.ok tem de ser verificado antes que o corpo tenha qualquer significado.
A armadilha específica do Node é a do proxy, e vale a pena repeti-la porque falha de forma tão silenciosa: o fetch integrado ignora completamente o HTTP_PROXY. Se precisar de tráfego por proxy, defina um despachante explicitamente — e, em seguida, confirme o endereço de saída, porque uma configuração que não faz nada parece exatamente igual a uma que funciona.
