JavaScript (TypeScript) のコードから HTTP リクエストを送る手段として, 最近では Web 標準の一つである Fetch Standard で定義された fetch() が使われることが多いですね.
await fetch("https://example.com");
リクエストヘッダーには Host を設定できない
Fetch Standard では Host をはじめとして Content-Length, Cookie, Origin など, いくつかのリクエストヘッダーを設定 (JavaScript から上書き) することが禁止されています.
いずれのヘッダーも HTTP やセキュリティ上の取り決めに従うために, ページの JavaSript が任意に設定するのではなく, ブラウザ側で設定されるべきものであると言えるでしょう.
サーバーサイドでも Host を設定できない (ことがある)
問題はここからで, 近年では fetch() はブラウザ環境だけにとどまらず, サーバー環境でも標準的な手段として使われるようになっています.
一方で Fetch Standard で定義された一部リクエストヘッダーの禁止などは基本的にブラウザ環境を念頭に置いたものであるため, サーバー環境から見るとナンセンスなものになっていることがあります.
たとえば Host ヘッダーの設定については, サーバー環境やブラウザ外で実行するスクリプトにおいては, リクエストのプロキシやサーバーの動作チェックなど, 一定のユースケースがあり得ます.
await fetch("http://localhost:8080", { headers: { Host: "example.com" }, });
ところが上記のスクリプトを各種サーバー環境 / ライブラリで動かしてみると, 以下の環境では headers に指定した値 example.com は無視され, リクエストの Host ヘッダーには localhost:8080 が設定されます.
- Node.js v24.7.0
- Deno v2.4.5
- Cloudflare Workers (miniflare@4.20250823.1 で確認)
- undici@7.15.0 (Node.js に組み込まれているライブラリ)
とはいえ全てのサーバ環境で足並みが揃っているわけではなく, 以下の環境では headers に指定した値 example.com がそのまま使われます.
- Bun v1.2.21
- node-fetch@3.3.2
これは一見すると環境によって Fetch Standard への準拠度合いが異っているということのように見えるのですが, 実はそう簡単な話ではありません.
話はそこまで単純じゃない
確かにサーバー環境で Host ヘッダーの設定が禁止されているのは, それが Fetch Standard で禁止されているからというので説明できるように見えます. 一方でこれらの環境の変更の履歴や実際の挙動を追ってみると, どうやらそのように説明するのはあまり正しくなさそうです.
おそらくサーバー環境で最初に Host ヘッダーの設定を禁止したのは Deno で, そのきっかけとしては受け取った Request オブジェクト (そう, サーバーはリクエストを受け取る側でもあるのです) を再利用したことで意図せず Host ヘッダーを設定してしまうトラブルがあったようです.
これはアプリケーション側で解決すべき問題のような気もするのですが, 実行環境の側で解決されることになったようです (明言はないものの, もしかするとその背景に Fetch Standard の存在があったのかもしれません).
これに合わせて Fetch Standard で禁止されたヘッダーを設定できてしまうという issue も閉じられているのですが, 上記の変更で禁止されたのはあくまで Host ヘッダーのみで, Cookie や Origin などのヘッダーについては設定が可能なまま現状維持という方向性のようです.
Node.js (undici) ではリダイレクトが発生した際のトラブルをきっかけとして Host ヘッダーを禁止しています.
やはり Fetch Standard で禁止されたヘッダーを設定できてしまうという issue も閉じられているのですが, こちらも同じく禁止されたのは Host ヘッダーのみで, Cookie や Origin などは引き続き設定できます.
ちなみに node-fetch にも undici と同様の issue が作成されていますが, こちらは未解決のままとなっています.
またサーバー環境で Host ヘッダーの設定を禁止している仕組みについても, Fetch Standard とは全く異なるものになっています.
Fetch Standard によれば, リクエストに使う Headers オブジェクトに対しては, 禁止されたヘッダーを設定すること自体ができません.
以下のスクリプトをブラウザで実行すると, headers には Host ヘッダーを設定できて [["Host", "example.com"]] が出力されるのに対して, reqHeaders には設定できず [] が出力されることが確認できます.
// 文脈がなければ任意のヘッダーを設定できる const headers = new Headers(); headers.append("Host", "example.com"); console.log([...headers]); // => [["Host", "example.com"]] // リクエストに使う場合は Host など一部のヘッダーは設定できない const reqHeaders = new Request("http://localhost:8080").headers; reqHeaders.append("Host", "example.com"); console.log([...reqHeaders]); // => []
一方でサーバー環境でこのスクリプトを実行すると, 上で挙げたいずれの環境でも, headers と reqHeaders 共に Host ヘッダーを設定できて [["Host", "example.com"]] が出力されます.
最終的な HTTP リクエストで Host ヘッダーが設定されない (headers に設定した値で上書きされない) のは, より後段の実際にリクエストを送信する部分で処理を行なっているようです.
まとめると, いずれのサーバー環境でも Fetch Standard で定義されたリクエストヘッダーの禁止には基本的には従っておらず, Host ヘッダーが禁止されている場合も (Fetch Standard が理由づけの一つであった可能性はあるものの) 主な理由は別にあったようです.
なお WinterCG (現 WinterTC) でサーバー環境にも適合するように Fetch Standard を変更する試みがあり, 特に Cookie や Origin などの一部リクエストヘッダの解禁について手が入れられていたようですが, その後これをサーバー環境の新たな標準としたり Fetch Standard 本体が変更を取り込むところまでは現在のところは至っていないようです.
おまけ: 各種サーバー環境で Host ヘッダーを指定する方法
Node.js の場合は fetch() の代わりに undici.request() などを使えば Host ヘッダーを設定できます.
import { request } from "undici"; await request("http://localhost:8080", { headers: { Host: "example.com" }, });
Deno の場合は fetch() に拡張オプション client が用意されており, ここに allowHost オプションを有効にした Deno.HttpClient を渡すことで Host ヘッダーの設定が可能です.
await fetch("http://localhost:8080", { headers: { Host: "example.com" }, client: Deno.createHttpClient({ allowHost: true }), });
あるいはそもそも Host ヘッダーを通常と異なる値に設定しようとする前に, 代わりに Forwarded や X-Forwarded-Host など Host とは別の / より適したヘッダーが使えないかも検討しましょう.