ここで注意すべき特定の落とし穴が1つあります。私たちは Geonode というプロキシ販売業者ですが、Node.jsに組み込まれているfetchは、HTTP_PROXYおよびHTTPS_PROXYという環境変数を完全に無視してしまいます。 エコシステム内の他のすべてのHTTPクライアントはこれらの環境変数を正しく処理するため、ユーザーはプロキシを設定し、リクエストが成功するのを確認すると、正常に動作していると誤解してしまいます――実際にはトラフィックが直接送信されているのです。 警告もエラーも表示されません。修正方法は数行で済みます。詳細は以下のプロキシのセクションに記載されています。Nodeのfetchへのトラフィックをプロキシ経由で送信しており、かつdispatcherを明示的に設定していない場合、リクエストはほぼ確実にプロキシを経由していません。
ネイティブのfetchか、それともnode-fetchパッケージか?
まずここから始めましょう。これが、何をインストールするかを決定するからです。
Nodeのドキュメントによると、fetchはv17.5.0およびv16.15.0で追加され、v18.0.0から--experimental-fetchフラグの制限が解除され、v21.0.0で「実験段階を終了」したと記載されています。 これは、「Node.js用にゼロから書き直されたHTTP/1.1クライアントであるundiciに基づく、fetch()関数のブラウザ互換実装」と説明されています。Headers、Request、およびResponseも同様の経緯をたどっています。
したがって、現在サポートされているどのNodeバージョンにおいても、fetchはグローバル変数として利用可能であり、依存関係を追加する必要はありません。
node-fetchパッケージが有用なのは、古いランタイム上でコードを維持する場合と、そのAPIが異なるごく少数の動作が必要となる場合の2つのケースに限られます。なお、バージョン3はESM専用であるため、requireを依然として使用しているプロジェクトでは問題が発生する可能性があります。
APIは意図的に同一に設計されているため、以下の内容は両方に適用されます。
ヘッダーを設定する3つの方法
プレーンなオブジェクト — 一般的なケースであり、ほとんどの場合にこれを使用します:
const res = await fetch("https://api.example.com/items", {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer eyJhbG...",
"Accept": "application/json",
},
});
**Headers
オブジェクト** — 条件に応じて設定を構築する場合:
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 });
ペアの配列 — ヘッダーが正当な理由で繰り返される場合に便利:
const res = await fetch(url, {
headers: [
["Accept", "application/json"],
["X-Trace", "a"],
["X-Trace", "b"],
],
});
単純なケースでは、これら3つはすべて同等です。Headers
オブジェクトが真価を発揮するのは、条件分岐が必要な場合や、送信前に構築した内容を検査したい場合です。
``set`
対 ``append
`:意外な違い。
MDNのドキュメント によると、set()
は「既存のヘッダーに新しい値を設定し」、既存の値を上書きするのに対し、append()
は「既存のヘッダーに新しい値を追加するか、ヘッダーが存在しない場合は新規に追加する」とされています。
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
set
は値を「追加」するのに対し、は既存の値を「上書き」します。送信するほぼすべてのヘッダーにおいて、set
が適切な選択となります。Authorization
の値を2つ送信しても、意味のあるリクエストにはなりません。append
は、複数の値が許容されるヘッダーにおいて重要ですが、実際にはそのようなヘッダーはごくわずかです。
ヘッダー名は大文字小文字を区別しません。 MDN によると、すべてのメソッドにおいて「大文字小文字を区別しないバイトシーケンスで照合される」ため、h.get("content-type")
と h.get("Content-Type")
は同じ値を返します。可読性を考慮して規約を決め、それ以上気にする必要はありません。
また、存在を確認するには has()
、削除するには delete()
、そして Set-Cookie
のすべての値の配列を返す getSetCookie()
があります。このヘッダーは、複数の値が実際に共存する主なケースであるため、単純な get()
を使用すると、それらを連結して信頼性のある方法で分割できないものになってしまうため、 が必要となります。
設定できないヘッダー
設定したヘッダーが表示されない理由。
MDN では、Headers オブジェクトには、何が変更可能かを決定する「ガード」が設けられていると説明されています。 単独のnew Headers()には制限がありません。Requestに付加されたヘッダーは、「forbidden以外のリクエストヘッダー」の変更を許可します。また、「Response.error()、Response.redirect()、またはfetch()」から取得されたResponse上のヘッダーは不変であり、レスポンスを受信した後にそのヘッダーを変更することはできません。
Forbiddenリクエストヘッダーはランタイムによって制御されるものであり、これらを設定しようとしてもエラーが発生するのではなく、黙って無視されます。このリストには、Host、Connection、Content-Length、Transfer-Encoding、Origin、特定のコンテキストにおけるReferer、およびSec-やProxy-で始まる一連のヘッダーが含まれます。
これには2つの実用的な影響があります。
「何も起こらないこと」が失敗モードです。 例外も警告も発生せず、単にヘッダーが送信されないだけです。サーバーが、設定したヘッダーを受信していないと主張する場合は、コードを再確認するのではなく、実際にネットワーク上で何が送信されたかを確認してください。
Node.jsは、これらのうちいくつかについてブラウザよりも寛容です。これは、保護すべきオリジンが存在しないためです。 Nodeで正常にヘッダーを設定できたコードでも、ブラウザではそのヘッダーが無視されてしまうことがあり、これは共有コードにとって深刻な移植性の落とし穴となります。
実際に何が送信されたかを確認するには、送信した内容をそのまま返してくれるサービスにリクエストを送ってみてください:
const res = await fetch("https://httpbin.org/headers", {
headers: { "X-Test": "value", "User-Agent": "MyBot/1.0" },
});
console.log(await res.json());
レスポンスヘッダーの読み取り
残りの半分についてですが、知っておくべき挙動が1つあります。
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);
}
ヘッダーセットは正規化されるため、反復処理の結果、名前は小文字になります。
** ``Set-Cookie`
には特別な処理が必要です。** 複数のクッキーは複数のヘッダーとして届き、単純な ``get("set-cookie")
を実行すると、それらがコンマで区切られて返されます。しかし、クッキーの値自体に、Expires`
の日付情報としてコンマが含まれている可能性があるため、これは曖昧です。まさにこの問題に対処するために ``getSetCookie()`
` が存在し、配列を返します:
const cookies = res.headers.getSetCookie();
レスポンスヘッダーは不変です。 fetch
が返した内容を変更することはできません。変更したバージョンが必要な場合は、新しい Response
を生成してください。
そして、どのヘッダーよりも重要な確認事項があります。fetch
は、HTTPエラーステータスによってリクエストを拒否しません。404や500でも通常通り解決されてしまうため、本文を解釈する前にres.ok
を必ずテストする必要があります。
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
これを省略することが、.json()
の呼び出しでUnexpected token '<'
が発生する直接的な原因となります。つまり、エラーページを解析してしまったのです。
デフォルトのヘッダーとNodeが追加するヘッダー
Nodeはいくつかのヘッダーを自動的に設定します。これらが何であるかを把握しておくと、混乱を防ぐことができます。
Host — URLから導出されるもので、設定することはできません。
Connection — 接続プールによって管理されます。
Content-Length — ボディから算出されます。
Accept — 設定しない限り、デフォルトは */* です。
Accept-Encoding — Node は圧縮のサポートを通知し、レスポンスを透過的に解凍します。
User-Agent — Nodeはデフォルトで独自のユーザーエージェントを送信します。通常はundiciを識別します。
最後の点は、サードパーティと通信するあらゆるケースにおいて重要です。デフォルトのランタイムユーザーエージェントは正確な識別情報ですが、自動化されたクライアントにとっては不適切なものです。連絡先URLを含む正直な名前の方が、匿名のランタイム文字列よりも好意的に扱われます:
headers: { "User-Agent": "AcmeBot/1.0 (+https://acme.example.com/bot)" }
ボディに関する注意点:FormDataオブジェクトを渡す際、Content-Typeを自分で設定しないでください。 これにはマルチパート境界が含まれているため、ランタイムがこれを生成する必要があります。これを上書きすると、サーバーが解析できないリクエストが生成されてしまいます。これは、原因不明の400や415エラーの最も一般的な原因の一つです。
すべてのリクエストに対するヘッダーの設定
スクリプト以外のものについては、一元管理しましょう。
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;
}
ここには、特に重要な2つのポイントがあります。最初にデフォルト値を展開することで、呼び出し元はそれらのいずれでも上書きできるようになり、これが望ましい動作です。また、エラー本文の最初の200文字を含めることで、意味不明なステータスコードを、具体的なアクションにつながるメッセージに変えることができます。
なお、オブジェクトのスプレッドは浅く、キー文字列が完全に一致する場合にのみ適用されるため、呼び出し側のオプションに "content-type" を指定しても、デフォルトの "Content-Type" は上書きされません。両方が送信され、実行時にいずれか一方が選択されます。呼び出し側が任意の大文字小文字を使用する可能性がある場合は、代わりに Headers オブジェクトを作成し、大文字小文字を区別しない set() にマージを適切に処理させましょう。
機能しないヘッダーのデバッグ
ほぼすべてのヘッダーの問題を数分で解決できる手順です。可能性を最も多く排除できる順序で進めます。
第一に――実際にネットワーク上で何が送信されたかを確認する。 これを行わない限り、このリストの他の項目は意味をなしません。ヘッダーエコーサービスを利用するのが最も手っ取り早い方法です:
const res = await fetch("https://httpbin.org/headers", { headers: myHeaders });
console.log(JSON.stringify(await res.json(), null, 2));
ここでヘッダーが見つからない場合、そのヘッダーはプロセスから送信されていません。つまり、禁止されているか、スペルミスがあるか、上書きされているかのいずれかです。ここでヘッダーが見つかるにもかかわらず、ターゲット側で異なる結果が返される場合は、送信元とターゲットの間のどこかでヘッダーが削除されています。
2 — 送信前にHeadersオブジェクトを作成し、それを検査する。 これにより、「構築に誤りがあった」場合と「ランタイムによって削除された」場合を区別できます:
const h = new Headers(myHeaders);
console.log([...h.entries()]);
Headersオブジェクトの構築時には、ランタイムが行うのと同じ正規化処理が適用されるため、ここで残った名前こそが実際に送信される名前となります。
3 — 意図しない重複がないか確認する。 浅いマージの落とし穴:オブジェクトのスプレッド演算子は、キーを文字列として完全に一致させるため、{...{"Content-Type": "a"}, ...{"content-type": "b"}} を実行すると両方のエントリが生成されてしまいます。呼び出し元が任意の大文字小文字の区別を行う可能性がある場合は、Headers オブジェクトを作成し、set() を使用してください。後者は大文字小文字を区別しないマッチングを行うため、正しくマージされます。
4 — curl で再現する。 同じリクエストがターミナルからは動作するが Node からは動作しない場合、違いはサーバー側ではなくコード側にあります:
curl -v -H "Authorization: Bearer $TOKEN" https://api.example.com/items 2>&1 | grep '^>'
2つの > ブロックを並べて比較すると、通常、違いが明らかになります。
5 — ステータスだけでなく、レスポンス全体を読み取る。 400や401のレスポンスには、どのヘッダーが不正だったかを正確に説明する本文が含まれていることが多く、それを破棄するコードは回答そのものを捨ててしまっていることになります:
if (!res.ok) console.error(res.status, (await res.text()).slice(0, 300));
また、リダイレクトも確認してください。 fetch はデフォルトでリダイレクトを追跡しますが、リダイレクトによってオリジンが異なる場合、一部のヘッダー(特に Authorization)は破棄されます。元のリクエストでは失敗したが、最終的な URL への直接リクエストでは成功する場合、その原因がほぼ間違いなくこれです。
プロキシに関する注意点
他のすべての Node HTTP クライアントとは異なる動作であり、このセクションが存在する理由でもあります。
**Node の ``fetch`
は、``HTTP_PROXY
、``HTTPS_PROXY
、``NO_PROXY
` を参照しません。** これらを設定しても何も変わりません。リクエストは直接送信され、成功しますが、プロキシがバイパスされたことを示す兆候は一切ありません。
この問題を解決するには、undici氏の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");
プロセス全体ではなく単一のリクエストに対して適用するには、呼び出しごとにディスパッチャーを渡します:
const agent = new ProxyAgent("http://proxy.example.com:9000");
const res = await fetch(url, { dispatcher: agent });
なお、dispatcher
は標準のFetch APIの一部ではなく、Node固有の拡張機能であるため、これを使用したコードはブラウザには移植できません。
必ず適用されたことを確認してください。 ディスパッチャーの有無で、サービスが認識しているアドレスを確認します:
const res = await fetch("https://api.ipify.org?format=json");
console.log(await res.json());
アドレスが変わらない場合、プロキシはパスに含まれていません。エラーとして通知されることもないため、この確認こそが、正常に動作する設定と、気付かれないままバイパスされてしまう設定を見分ける唯一の手段となります。 これは、プロキシのテストが重要な理由で取り上げたのと同じ種類の「静かな失敗」です。
ここでのHTTPクライアント間の動作の違いは、まさにaxiosとfetchの比較で取り上げたようなものです。
よくある質問
node-fetch でヘッダーを設定するにはどうすればよいですか?
オプションに headers オブジェクトを渡します:fetch(url, { headers: { "Authorization": "Bearer ..." } })。また、Headers インスタンスや、名前と値のペアの配列を渡すこともできます。この構文は、Node.js の組み込み関数 fetch でも同様に使用できます。
依然として node-fetch パッケージは必要ですか?
通常は必要ありません。Node.js では v17.5.0 以降、fetch がグローバル変数として利用可能であり、v18 以降ではフラグなしで、v21 以降では安定版として提供されています。古いランタイム環境や、特定の動作上の違いがある場合のみ、このパッケージをインストールしてください。なお、バージョン 3 は ESM 専用である点に注意してください。
headers.set と headers.append の違いは何ですか?
set は、そのヘッダーの既存の値をすべて上書きします。append は別の値を追加するため、2回 append するとカンマ区切りのリストになります。ほとんどの場合は set を使用してください。append は、複数の値が有効なヘッダーでのみ重要です。
なぜヘッダーが送信されないのですか?
おそらく、ランタイムによって制御される禁止ヘッダーが原因です。その例としては、Host、Connection、Content-Length、およびSec-ファミリーなどが挙げられます。これらはエラーを発生させるのではなく、黙って無視されます。ヘッダーエコーサービスにリクエストを送信して、実際にネットワーク上で何が送信されたかを確認してください。
fetch におけるヘッダー名は大文字と小文字が区別されますか?
いいえ。MDN では、すべての Headers メソッドにおいて、ヘッダー名の照合は大文字小文字を区別しないバイトシーケンスで行われると規定されています。したがって、get("content-type") と get("Content-Type") は同等です。Headers オブジェクトを反復処理すると、小文字のヘッダー名が得られます。
複数の Set-Cookie ヘッダーを読み取るにはどうすればよいですか?
res.headers.getSetCookie() を使用してください。これは配列を返します。単純な get("set-cookie") を使用すると、それらがコンマで結合されますが、Expires の日付のように、クッキーの値自体にコンマが含まれている場合があるため、これは曖昧になります。
なぜNode fetchはHTTP_PROXY設定を無視するのですか?
他のほぼすべてのNode HTTPクライアントとは異なり、fetchはこれらの環境変数を一切読み取らないためです。undiciのProxyAgentをsetGlobalDispatcherと組み合わせて使用するか、リクエストごとにdispatcherを渡してください。なお、プロキシがバイパスされてもエラーは発生しないため、出口アドレスを必ず確認してください。
FormData を送信する際、Content-Type を設定すべきですか?
いいえ。ランタイムがマルチパート境界を含めて自動的に生成するため、手動で設定すると境界が削除され、サーバーが解析できないリクエストが生成されてしまいます。これは、原因不明の 400 や 415 レスポンスが発生する一般的な原因です。
まとめ
Node.jsでヘッダーを設定するのは、どのAPIを使用する場合でも1行で済みます。また、組み込みのfetchがあるため、ほとんどのプロジェクトではもはやこのためのパッケージを一切必要としません。
混乱の原因となるのは、主に3つの挙動です。setはヘッダーを置換しますが、appendはヘッダーを累積します。これを逆にしてしまうと、サーバーが拒否するカンマ区切りのヘッダー値が生成されてしまいます。 許可されていないヘッダーはエラーを発生させることなく黙って破棄されるため、サーバーが受信していないヘッダーについては、エディタで再読み込みするのではなく、通信経路上で検証する必要があります。また、fetchはHTTPエラー時に解決されるため、res.okは、ボディが意味を持つようになる前に確認する必要があります。
Node固有の落とし穴はプロキシに関するもので、これが非常に静かに失敗するため、繰り返し強調する価値があります。組み込みのfetchは、HTTP_PROXYを完全に無視します。プロキシ経由のトラフィックが必要な場合は、ディスパッチャを明示的に設定してください。そして、何も処理しない設定は正常に動作している設定と全く同じように見えるため、出口アドレスを必ず確認してください。
