Fetch APIにおけるクロスオリジンリクエストの全貌と安全な設計戦略
Webアプリケーション開発において、異なるドメイン、プロトコル、またはポート間でリソースを共有するクロスオリジンリクエストは避けて通れないテーマです。Fetch APIの登場により、従来のXMLHttpRequestよりも直感的で柔軟な通信が可能になりましたが、同時にブラウザのセキュリティモデルである「同一生成元ポリシー(Same-Origin Policy)」を正しく理解し、適切にハンドリングすることがエンジニアには強く求められています。本稿では、Fetch APIを用いたクロスオリジンリクエストの仕組み、CORSの技術的詳細、そして実務における堅牢な実装パターンについて深掘りします。
同一生成元ポリシーとクロスオリジンリクエストの基礎
ブラウザのセキュリティの根幹を成す「同一生成元ポリシー(Same-Origin Policy: SOP)」は、あるオリジン(スキーム、ホスト、ポートの組み合わせ)から読み込まれたドキュメントやスクリプトが、別のオリジンのリソースと対話することを制限する仕組みです。これにより、悪意のあるサイトがユーザーの認証情報を悪用して他サイトからデータを盗み出すといった攻撃を防止しています。
しかし、現代のマイクロサービスアーキテクチャやAPIエコノミーにおいて、ドメインを跨いだリソース共有は不可欠です。ここで登場するのがCORS(Cross-Origin Resource Sharing)です。CORSは、HTTPヘッダーを使用して、ブラウザに対して特定のオリジンからのリクエストを許可するようサーバー側から指示を出すための仕組みです。Fetch APIを使用する場合、ブラウザはこのCORSの仕様に従い、必要に応じて自動的に「プリフライトリクエスト」を送信します。
CORSのメカニズム:単純リクエストとプリフライト
Fetch APIでクロスオリジンリクエストを行う際、リクエストの内容によってブラウザの挙動が異なります。
1. 単純リクエスト(Simple Requests):
特定の条件(GET/POST/HEADメソッド、特定のContent-Typeなど)を満たす場合、ブラウザは即座にリクエストを送信します。サーバー側はレスポンスヘッダーに「Access-Control-Allow-Origin」を含めることで許可を示します。
2. プリフライトリクエスト(Preflight Requests):
PUTやDELETEなどのメソッドを使用したり、カスタムHTTPヘッダー(Authorizationなど)を付与したりする場合、ブラウザは本体のリクエストを送る前に、OPTIONSメソッドによるプリフライトリクエストを送信し、サーバーがそのリクエストを許可するかどうかを事前に確認します。このラウンドトリップがパフォーマンスに与える影響を考慮することは、フロントエンドエンジニアの重要な責務です。
Fetch APIによる実装とモードの制御
Fetch APIの `mode` オプションは、リクエストの性質を決定する重要な設定です。
– `cors`: デフォルトの挙動。クロスオリジンリクエストを許可します。
– `no-cors`: 限定的なリクエスト。主にCDNや画像などのリソース取得用で、レスポンスの内容をJavaScriptで読み取ることはできません。
– `same-origin`: 同一オリジンのみ許可し、それ以外はエラーとなります。
また、認証情報(CookieやHTTP認証)を伴うリクエストを行う場合は、`credentials` オプションを `include` に設定する必要があります。このとき、サーバー側の「Access-Control-Allow-Origin」はワイルドカード(*)ではなく、具体的なオリジンを指定しなければならないという制約が発生します。
サンプルコード:安全なクロスオリジンリクエストの構築
以下に、認証情報を含めたクロスオリジンリクエストの推奨実装例を示します。
/**
* クロスオリジンAPIへのセキュアなリクエスト関数
* @param {string} url - 宛先URL
* @param {Object} options - リクエストオプション
*/
async function fetchSecureApi(url, options = {}) {
const defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
const config = {
...options,
method: options.method || 'GET',
headers: { ...defaultHeaders, ...options.headers },
// 認証情報(Cookie等)を送信するために必要
credentials: 'include',
// クロスオリジンリクエストであることを明示
mode: 'cors'
};
try {
const response = await fetch(url, config);
// HTTPエラー(4xx, 5xx)を捕捉
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
// 使用例
fetchSecureApi('https://api.example.com/v1/user-profile', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_TOKEN'
}
})
.then(data => console.log(data))
.catch(err => alert('データ取得に失敗しました'));
実務における注意点とトラブルシューティング
実務でクロスオリジンリクエストを扱う際、エンジニアが直面しやすい課題と解決策を整理します。
1. プリフライトのオーバーヘッド:
頻繁にリクエストが発生する場合、OPTIONSリクエストの待ち時間がボトルネックになります。サーバー側で「Access-Control-Max-Age」ヘッダーを適切に設定し、プリフライトの結果をブラウザにキャッシュさせることで、パフォーマンスを大幅に向上させることが可能です。
2. 認証情報の取り扱い:
`credentials: ‘include’` を使用する場合、サーバー側の設定も厳格である必要があります。特に `Access-Control-Allow-Origin` に `*` を指定していると、多くのブラウザはセキュリティエラーを返します。必ず許可するオリジンを明示的に指定するか、バックエンドで動的にリクエスト元のオリジンを検証するロジックを実装してください。
3. エラーハンドリングの罠:
Fetch APIは、ネットワークエラーが発生したときやCORSで拒否されたときにはPromiseを拒否しますが、HTTPステータスコードが4xxや5xxであってもPromiseを解決してしまいます。必ず `response.ok` プロパティをチェックするラッパー関数を定義することが、堅牢なフロントエンド構築の定石です。
4. 開発環境と本番環境の乖離:
ローカル開発時に `localhost` でAPIを叩く際、CORSで躓くことがよくあります。開発中はプロキシサーバー(Webpack Dev ServerやViteのproxy機能)を使用してオリジンを一致させ、本番環境ではAPI GatewayやNginxで適切なCORSヘッダーを付与する構成が、最もクリーンで管理しやすいアプローチです。
セキュリティの観点から:CORSは万能ではない
CORSを理解することは重要ですが、過度に依存しすぎるのも危険です。CORSはあくまでブラウザが提供する「礼儀正しいリクエスト」のためのルールです。サーバー側で適切な認証(JWTやセッション管理)を怠れば、CORSの設定がどれほど厳格であっても、セキュリティホールは生まれます。
また、CSRF(Cross-Site Request Forgery)対策についても考慮が必要です。CORS設定を緩めすぎると、悪意のあるサイトからのリクエストを許容してしまうリスクが高まります。特に `Access-Control-Allow-Credentials: true` を設定する場合は、リクエストを送信できるオリジンを信頼できるドメインに限定することを徹底してください。
まとめ
Fetch APIにおけるクロスオリジンリクエストは、現代のWeb開発において避けては通れない技術領域です。単に「CORSエラーを消す」ことだけを目的とせず、その背景にある同一生成元ポリシーの意図、プリフライトリクエストの仕組み、そしてセキュリティリスクを包括的に理解することが、シニアエンジニアへの第一歩となります。
本稿で紹介した実装パターンや注意点をベースに、各プロジェクトの要件に合わせて最適な設計を行ってください。安定したAPI通信は、ユーザー体験の向上とシステムの堅牢性を支える最も重要な基盤の一つです。常に最新のブラウザ仕様を追いかけ、セキュリティとパフォーマンスのバランスを最適化し続けることが、我々フロントエンドエンジニアの使命です。

コメント