Bei uns gibt es einen ganz bestimmten Haken: Wir sind Geonode und verkaufen Proxys, und die in Node integrierte Funktion „fetch“ ignoriert die Umgebungsvariablen „HTTP_PROXY“ und „HTTPS_PROXY“ vollständig. Jeder andere HTTP-Client im Ökosystem berücksichtigt diese Variablen, sodass Nutzer einen Proxy konfigurieren, sehen, dass Anfragen erfolgreich sind, und davon ausgehen, dass er funktioniert – während der Datenverkehr direkt weitergeleitet wird. Es gibt weder eine Warnung noch einen Fehler. Die Lösung umfasst nur wenige Zeilen und ist im Abschnitt zum Proxy weiter unten beschrieben. Wenn Sie den Datenverkehr von Node-fetchs über einen Proxy leiten und keinen Dispatcher explizit festgelegt haben, werden Ihre Anfragen mit ziemlicher Sicherheit nicht über Ihren Proxy geleitet.
Native Fetch oder das node-fetch-Paket?
Fangen Sie hier an, denn davon hängt ab, was Sie installieren.
In der Dokumentation von Node wird „fetch“ als in den Versionen v17.5.0 und v16.15.0 hinzugefügt aufgeführt, ab Version v18.0.0 nicht mehr hinter dem Flag „--experimental-fetch“ verborgen und ab Version v21.0.0 als „nicht mehr experimentell“ eingestuft. Es wird beschrieben als „eine browserkompatible Implementierung der Funktion fetch(), basierend auf undici, einem von Grund auf für Node.js geschriebenen HTTP/1.1-Client“. Headers, Request und Response folgen demselben Zeitplan.
Auf jeder derzeit unterstützten Node-Version ist fetch also eine globale Variable, und Sie benötigen keine Abhängigkeit.
Das Paket „node-fetch“ bleibt in zwei Fällen nützlich: bei der Pflege von Code auf einer älteren Laufzeitumgebung und wenn eines der wenigen Verhaltensweisen benötigt wird, bei denen sich die API unterscheidet. Beachten Sie, dass Version 3 ausschließlich ESM-basiert ist, was Projekte behindert, die noch „require“ verwenden.
Alles im Folgenden gilt für beide, da die API bewusst identisch gehalten wurde.
Die drei Möglichkeiten, Header festzulegen
Ein einfaches Objekt – der gängige Fall und die in den meisten Fällen zu verwendende Variante:
const res = await fetch("https://api.example.com/items", {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer eyJhbG...",
"Accept": "application/json",
},
});
**Ein „Headers
“-Objekt** – wenn Sie die Konfiguration bedingt aufbauen:
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 });
Ein Array von Paaren – nützlich, wenn sich ein Header berechtigterweise wiederholt:
const res = await fetch(url, {
headers: [
["Accept", "application/json"],
["X-Trace", "a"],
["X-Trace", "b"],
],
});
Alle drei sind im einfachen Fall gleichwertig. Das „Headers
“-Objekt bewährt sich, wenn Sie bedingte Logik benötigen oder wenn Sie vor dem Senden überprüfen möchten, was Sie erstellt haben.
„set
“ versus „append
“ – Der Unterschied, der für Überraschungen sorgt.
MDN-Dokumentation beschreibt „set()
“ als das Setzen „eines neuen Werts für einen bestehenden Header“ und das Überschreiben bestehender Werte, während „append()
“ „einen neuen Wert an einen bestehenden Header anhängt oder diesen hinzufügt, falls er nicht vorhanden ist“.
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
„accumulates“ sammelt; „set
“ ersetzt. Für fast jeden Header, den Sie senden, ist „set
“ die gewünschte Methode – das Senden von zwei Werten mit „Authorization
“ ist keine sinnvolle Anfrage. „append
“ ist für Header relevant, bei denen mehrere Werte zulässig sind, und in der Praxis ist dies nur eine kurze Liste.
Bei Header-Namen wird die Groß-/Kleinschreibung nicht berücksichtigt. MDN weist darauf hin, dass sie bei allen Methoden „nach einer Byte-Sequenz ohne Berücksichtigung der Groß-/Kleinschreibung abgeglichen werden“, sodass h.get("content-type")
und h.get("Content-Type")
denselben Wert zurückgeben. Legen Sie eine Konvention zur besseren Lesbarkeit fest und machen Sie sich darüber keine weiteren Gedanken.
Außerdem gibt es „has()
“, um das Vorhandensein zu prüfen, „delete()
“, um den Header zu entfernen, und „getSetCookie()
“, das ein Array aller „Set-Cookie
“-Werte zurückgibt – dies ist notwendig, da dieser Header der Hauptfall ist, in dem mehrere Werte tatsächlich nebeneinander existieren und ein einfaches „get()
“ diese zu etwas verkettete, das sich nicht zuverlässig aufteilen lässt.
Header, die Sie nicht festlegen können
Der Grund, warum ein von Ihnen konfigurierter Header nicht angezeigt wird.
MDN beschreibt eine „Sperre“ bei Headers-Objekten, die festlegt, was geändert werden darf. Ein eigenständiges new Headers() unterliegt keinen Einschränkungen. An ein Request angehängte Header erlauben die Änderung von „nicht verbotenen Request-Headern“. Und Header in einem Response, das „von Response.error(), Response.redirect() oder fetch()“ bezogen wurde, sind unveränderlich – Sie können die Header einer Antwort nach deren Empfang nicht mehr ändern.
Verbotene Request-Header sind solche, die von der Laufzeitumgebung kontrolliert werden, und Versuche, sie zu setzen, werden stillschweigend ignoriert, anstatt einen Fehler auszulösen. Die Liste umfasst Host, Connection, Content-Length, Transfer-Encoding, Origin, Referer in bestimmten Kontexten sowie die mit den Präfixen Sec- und Proxy- versehenen Header-Familien.
Daraus ergeben sich zwei praktische Konsequenzen.
Stillschweigen ist der Fehlermodus. Keine Ausnahme, keine Warnung; der Header wird einfach nicht gesendet. Wenn ein Server behauptet, etwas nicht zu erhalten, das Sie festgelegt haben, überprüfen Sie, was tatsächlich über die Leitung gesendet wurde, anstatt Ihren Code erneut zu lesen.
Node ist in einigen dieser Fälle toleranter als ein Browser, da es keine Herkunft zu schützen gibt. Code, der einen Header in Node erfolgreich setzt, kann feststellen, dass dieser in einem Browser ignoriert wird, was eine echte Portabilitätsfalle für gemeinsam genutzten Code darstellt.
Um zu überprüfen, was Sie tatsächlich gesendet haben, senden Sie eine Anfrage an einen Dienst, der diese zurückgibt:
const res = await fetch("https://httpbin.org/headers", {
headers: { "X-Test": "value", "User-Agent": "MyBot/1.0" },
});
console.log(await res.json());
Lesen von Antwort-Headern
Die andere Hälfte – und dabei gibt es ein Verhalten, das man kennen sollte.
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);
}
Bei der Iteration werden die Namen in Kleinbuchstaben ausgegeben, da der Header-Satz normalisiert wird.
** „Set-Cookie
“ erfordert eine spezielle Behandlung.** Mehrere Cookies werden als mehrere Header zurückgegeben, und ein einfacher Aufruf von „get("set-cookie")
“ liefert sie durch Kommas getrennt – was mehrdeutig ist, da Cookie-Werte selbst Kommas in einem „Expires
“-Datum enthalten können. Genau für diesen Fall gibt es „getSetCookie()
“, das ein Array zurückgibt:
const cookies = res.headers.getSetCookie();
Antwort-Header sind unveränderlich. Sie können den von ``fetch`
zurückgegebenen Wert nicht ändern. Wenn Sie eine geänderte Version benötigen, erstellen Sie ein neues ``Response
`.
Und die Überprüfung, die wichtiger ist als jeder Header: fetch
lehnt HTTP-Fehlerstatus nicht ab. Ein 404- oder 500-Fehler wird normal aufgelöst, daher muss res.ok
getestet werden, bevor Sie den Body auswerten.
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
Das Überspringen dieser Überprüfung ist die direkte Ursache für „Unexpected token '<'
“ bei einem Aufruf von .json()
– Sie haben eine Fehlerseite geparst.
Standard-Header und was Node hinzufügt
Node setzt automatisch mehrere Header, und zu wissen, welche das sind, beugt Verwirrung vor.
Host — wird aus der URL abgeleitet, kann nicht festgelegt werden.
Connection — wird vom Verbindungspool verwaltet.
Content-Length — wird aus dem Body berechnet.
Accept — ist standardmäßig auf „*/*“ gesetzt, sofern Sie ihn nicht ändern.
Accept-Encoding — Node gibt die Komprimierungsunterstützung an und dekomprimiert die Antwort transparent.
User-Agent — Node sendet standardmäßig einen eigenen User-Agent, der typischerweise „undici“ angibt.
Der letzte Punkt ist wichtig für jede Kommunikation mit Drittanbietern. Ein standardmäßiger Laufzeit-User-Agent ist zwar eine korrekte Identifikation, für automatisierte Clients jedoch eine schlechte — ein ehrlicher Name mit einer Kontakt-URL wird besser behandelt als eine anonyme Laufzeit-Zeichenkette:
headers: { "User-Agent": "AcmeBot/1.0 (+https://acme.example.com/bot)" }
Ein Hinweis zu den Body-Inhalten: Wenn Sie ein FormData-Objekt übergeben, sollten Sie Content-Type nicht selbst setzen. Die Laufzeitumgebung muss diesen Wert generieren, da er die Multipart-Grenze enthält; ein Überschreiben führt zu einer Anfrage, die der Server nicht analysieren kann. Dies ist eine der häufigsten Ursachen für unerklärliche 400- oder 415-Fehler.
Header für jede Anfrage festlegen
Alles, was über ein Skript hinausgeht, sollte zentralisiert werden.
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;
}
Zwei Details darin sind besonders erwähnenswert. Das vorherige Angeben von Standardwerten bedeutet, dass ein Aufrufer jeden dieser Werte überschreiben kann – genau das gewünschte Verhalten. Und das Einfügen der ersten 200 Zeichen des Fehlertextes verwandelt einen undurchsichtigen Statuscode in eine Meldung, auf die man reagieren kann.
Beachten Sie, dass die Objektverteilung flach ist und genau auf die Schlüsselzeichenfolge abgeglichen wird. Daher überschreibt „"content-type"“ in den Optionen des Aufrufers nicht „"Content-Type"“ in den Standardwerten – Sie senden beide, und die Laufzeit wählt eine davon aus. Wenn Aufrufer beliebige Groß- und Kleinschreibung verwenden dürfen, erstellen Sie stattdessen ein „Headers“-Objekt und lassen Sie dessen groß-/kleinschreibungsunabhängiges „set()“ die Zusammenführung ordnungsgemäß handhaben.
Fehlerbehebung bei nicht funktionierenden Headern
Eine Vorgehensweise, mit der sich fast jedes Header-Problem innerhalb weniger Minuten beheben lässt – in der Reihenfolge, die die meisten Möglichkeiten ausschließt.
Erstens: Prüfen Sie, was tatsächlich über die Leitung gesendet wurde. Nichts anderes auf dieser Liste ist von Bedeutung, solange Sie dies nicht getan haben. Ein Header-Echo-Dienst ist der schnellste Weg:
const res = await fetch("https://httpbin.org/headers", { headers: myHeaders });
console.log(JSON.stringify(await res.json(), null, 2));
Wenn Ihr Header hier fehlt, hat er Ihren Prozess nie verlassen – er ist unzulässig, falsch geschrieben oder wurde überschrieben. Wenn er hier vorhanden ist und das Ziel etwas anderes angibt, wird er irgendwo zwischen Ihnen und dem Ziel entfernt.
Zweitens – Erstellen Sie das „Headers“-Objekt und überprüfen Sie es vor dem Senden. Dies unterscheidet zwischen „Ich habe es falsch erstellt“ und „Die Laufzeitumgebung hat es verworfen“:
const h = new Headers(myHeaders);
console.log([...h.entries()]);
Beim Erstellen eines „Headers“-Objekts wird dieselbe Normalisierung angewendet wie von der Laufzeitumgebung, sodass ein Name, der hier bestehen bleibt, auch gesendet wird.
Drittens – auf versehentliche Duplikate prüfen. Die „Shallow-Merge“-Falle: Bei einer Objektverteilung werden Schlüssel anhand der exakten Zeichenkette abgeglichen, sodass „{...{"Content-Type": "a"}, ...{"content-type": "b"}}“ beide Einträge erzeugt. Erstellen Sie ein Headers-Objekt und verwenden Sie „set()“, wenn Aufrufer beliebige Groß-/Kleinschreibung verwenden können, da dessen groß-/kleinschreibungsunabhängiger Abgleich die Zusammenführung korrekt durchführt.
Viertens – reproduzieren Sie den Fehler in curl. Wenn dieselbe Anfrage von einem Terminal aus funktioniert, von Node jedoch nicht, liegt der Unterschied in Ihrem Code und nicht auf dem Server:
curl -v -H "Authorization: Bearer $TOKEN" https://api.example.com/items 2>&1 | grep '^>'
Ein nebeneinanderliegender Vergleich der beiden >-Blöcke macht den Unterschied in der Regel deutlich.
Fünf – lies die gesamte Antwort, nicht nur den Status. Ein 400- oder 401-Status enthält häufig einen Body, der genau erklärt, welcher Header falsch war, und Code, der diesen verworfen, wirft die Antwort weg:
if (!res.ok) console.error(res.status, (await res.text()).slice(0, 300));
Und überprüfen Sie die Weiterleitung. fetch folgt standardmäßig Weiterleitungen, und einige Header – insbesondere Authorization – gehen verloren, wenn eine Weiterleitung zu einer anderen Herkunft führt. Wenn eine Anfrage direkt an die endgültige URL funktioniert, aber nicht an die ursprüngliche, ist das mit ziemlicher Sicherheit der Grund dafür.
Die Proxy-Falle
Das Verhalten, das sich von jedem anderen Node-HTTP-Client unterscheidet, und der Grund, warum dieser Abschnitt existiert.
**Nodes ``fetch`
liest weder ``HTTP_PROXY
, noch ``HTTPS_PROXY
oder ``NO_PROXY
`.** Das Setzen dieser Variablen ändert nichts. Anfragen werden direkt weitergeleitet, sie sind erfolgreich, und nichts deutet darauf hin, dass der Proxy umgangen wurde.
Die Lösung bietet undici mit „ProxyAgent
“:
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");
Um dies nur für eine einzelne Anfrage statt für den gesamten Prozess zu nutzen, übergeben Sie pro Aufruf einen Dispatcher:
const agent = new ProxyAgent("http://proxy.example.com:9000");
const res = await fetch(url, { dispatcher: agent });
Beachten Sie, dass „dispatcher
“ eine Node-spezifische Erweiterung ist und nicht Teil der Standard-Fetch-API, sodass Code, der diese Funktion nutzt, nicht auf einen Browser übertragbar ist.
Überprüfen Sie immer, ob die Änderung wirksam wurde. Fragen Sie einen Dienst, welche Adresse er sieht – mit und ohne Dispatcher:
const res = await fetch("https://api.ipify.org?format=json");
console.log(await res.json());
Wenn sich die Adresse nicht ändert, befindet sich der Proxy nicht im Pfad – und da kein Fehler auftritt, der Sie darauf hinweist, ist diese Überprüfung das Einzige, was zwischen einer funktionierenden Konfiguration und einer stillschweigend umgangenen Konfiguration steht. Es handelt sich um dieselbe Art von stillschweigendem Fehler, über die wir in Warum das Testen von Proxys wichtig ist geschrieben haben.
Die Verhaltensunterschiede zwischen den HTTP-Clients sind genau das, was wir in axios vs. fetch verglichen haben.
Häufig gestellte Fragen
Wie setze ich Header mit node-fetch?
Übergeben Sie ein „headers“-Objekt in den Optionen: fetch(url, { headers: { "Authorization": "Bearer ..." } }). Sie können auch eine „Headers“-Instanz oder ein Array aus Name-Wert-Paaren übergeben. Die gleiche Syntax funktioniert auch mit dem in Node integrierten „fetch“.
Brauche ich das node-fetch-Paket noch?
In der Regel nicht. Node verfügt seit Version 17.5.0 über eine globale „fetch“, die seit Version 18 nicht mehr mit einem Flag versehen ist und seit Version 21 stabil ist. Installieren Sie das Paket nur für ältere Laufzeiten oder bei bestimmten Verhaltensunterschieden – und beachten Sie, dass Version 3 ausschließlich ESM-basiert ist.
Was ist der Unterschied zwischen headers.set und headers.append?
set ersetzt jeden vorhandenen Wert für diesen Header; append fügt einen weiteren Wert hinzu, sodass zwei Anfügungen eine durch Kommas getrennte Liste ergeben. Verwenden Sie set für fast alle Fälle – append ist nur für Header relevant, bei denen mehrere Werte zulässig sind.
Warum wird mein Header nicht gesendet?
Höchstwahrscheinlich handelt es sich um einen verbotenen Header, der von der Laufzeitumgebung kontrolliert wird – darunter Host, Connection, Content-Length und die Sec--Familie. Diese werden stillschweigend ignoriert, anstatt einen Fehler auszulösen. Senden Sie eine Anfrage an einen Header-Echo-Dienst, um zu sehen, was tatsächlich über das Netzwerk gesendet wurde.
Wird bei fetch bei Header-Namen die Groß-/Kleinschreibung beachtet?
Nein. MDN legt fest, dass Header-Namen bei allen Headers-Methoden anhand einer Byte-Sequenz ohne Berücksichtigung der Groß-/Kleinschreibung abgeglichen werden, sodass get("content-type") und get("Content-Type") gleichwertig sind. Das Durchlaufen eines Headers-Objekts liefert Namen in Kleinbuchstaben.
Wie lese ich mehrere „Set-Cookie“-Header aus?
Verwenden Sie „res.headers.getSetCookie()“, das ein Array zurückgibt. Ein einfaches „get("set-cookie")“ verbindet diese mit Kommas, was jedoch mehrdeutig ist, da Cookie-Werte selbst Kommas in einem „Expires“-Datum enthalten können.
Warum ignoriert „Node fetch“ meine „HTTP_PROXY“-Einstellung?
Weil es diese Umgebungsvariablen im Gegensatz zu fast jedem anderen Node-HTTP-Client überhaupt nicht liest. Verwenden Sie „undici’s ProxyAgent“ mit „setGlobalDispatcher“ oder übergeben Sie pro Anfrage einen „dispatcher“ – und überprüfen Sie anschließend die Ausgangsadresse, da ein umgangener Proxy keinen Fehler ausgibt.
Sollte ich beim Senden von FormData den Content-Type festlegen?
Nein. Die Laufzeitumgebung generiert ihn einschließlich der Multipart-Grenze, und wenn du ihn selbst festlegst, wird die Grenze entfernt, was zu einer Anfrage führt, die der Server nicht analysieren kann. Dies ist eine häufige Ursache für unerklärliche 400- und 415-Antworten.
Fazit
Das Setzen von Headern in Node ist unabhängig von der verwendeten API eine Sache von nur einer Zeile, und dank der integrierten Funktion „fetch“ benötigen die meisten Projekte dafür gar kein Paket mehr.
Drei Verhaltensweisen sind für fast die gesamte Verwirrung verantwortlich: „set“ ersetzt, während „append“ akkumuliert – verwechselt man diese beiden, entstehen durch Kommas getrennte Header-Werte, die von Servern abgelehnt werden. Unzulässige Header werden stillschweigend verworfen, anstatt einen Fehler auszulösen; daher muss ein Header, den der Server nicht erhält, direkt in der Übertragung überprüft werden, anstatt ihn in Ihrem Editor erneut zu lesen. Und „fetch“ wird bei HTTP-Fehlern aufgelöst, sodass „res.ok“ überprüft werden muss, bevor der Body überhaupt eine Bedeutung hat.
Die Node-spezifische Falle betrifft den Proxy, und es lohnt sich, darauf hinzuweisen, da sie so unbemerkt zu Fehlern führt: Die integrierte Funktion fetch ignoriert HTTP_PROXY vollständig. Wenn Sie Proxy-Verkehr benötigen, legen Sie explizit einen Dispatcher fest – und überprüfen Sie anschließend die Ausgangsadresse, da eine Konfiguration, die nichts bewirkt, genau so aussieht wie eine, die funktioniert.
