はじめに
Express.jsを使って簡易APIを作成、アクセスしようとすると、表題のエラーが発生しました。
解決方法とCORSについて学んだことをまとめます。
問題
異なるポートでサーバーを起動、ブラウザ上で実行すると下記のエラーが発生
エラー内容
Access to fetch at 'http://localhost:3000/api/' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
ポートを変えて以下のサーバーを起動していました。
- Viteプロジェクト:http://localhost:5173
- Express.js:http://localhost:3000
原因
ブラウザが同一オリジンポリシーを適用していたため。
同一オリジンポリシーとは?
ブラウザに搭載されたセキュリティ上の制約
MDNより引用
同一オリジンポリシーは重要なセキュリティの仕組みであり、あるオリジンによって読み込まれた文書やスクリプトが、他のオリジンにあるリソースにアクセスできる方法を制限するものです。
同一オリジンとは
リクエスト元、リクエスト先のドメイン(スキーム、ホスト、ポート番号)が同じであること
同一オリジンの例
http://localhost:5173/api → http://localhost:5173/users
また、自身とは異なるドメインに対してリクエスト送信することをクロスオリジン通信と呼びます。
クロスオリジンの例
- http://localhost:5173 → http://localhost:3000 (今回のエラー、ポートが異なる)
- http://localhost:5173 → https://localhost:5173 (スキームが違う)
- http://example.com → http://api.example.com (ホストが違う)
クロスオリジン通信の場合、ブラウザがこのリクエストは危険と判断しエラーを返します。
そのため、表題のエラーが発生していました。
解決方法
CORSの仕組みを使い、http://localhost:3000 で定義したサーバー側に応答ヘッダを定義する
CORSとは
CORS(Cross-Origin Rescource Sharing)
同一オリジンポリシーによる制限を解除するために考えられた仕組み
MDNより引用
オリジン間リソース共有 (Cross-Origin Resource Sharing, CORS) は、 HTTP ヘッダーベースの仕組みを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行します。
オリジン間リクエストとは、例えば https://domain-a.com で提供されているウェブアプリケーションのフロントエンド JavaScript コードが fetch() を使用して https://domain-b.com/data.json へリクエストを行うようなものです。
CORSの動作フロー
以下の流れでクロスオリジンリクエストの許可のやりとりを行います。
- ブラウザ:「このAPIにアクセスしていいか?」(OPTIONSリクエスト)
- サーバー:「このオリジンからならOK」(Access-Control-Allow-Origin)
- ブラウザ:「本番のリクエスト送ります」(実際のGET/POST等)
1.OPTIONSリクエスト(プリフライトリクエスト)
HTTPヘッダをつかってOPTIONSメソッドによるリクエストを行います(条件によって省略可)
HTTPヘッダには以下を定義する
| ヘッダ | 中身 |
|---|---|
| Origin | リクエスト要求者のオリジン |
| Access-Control-Request-Method | 送信を許可してほしいHTTPメソッド |
| Access-Control-Request-Headers | 送信を許可してほしいHTTPヘッダ |
2.リクエストの判断を行い、レスポンスの返却
サーバーはリクエストを受けると以下の情報をレスポンスとして返却します
| ヘッダ | 中身 |
|---|---|
| Access-Control-Allow-Origin | 送信を許可するオリジン |
| Access-Control-Allow-Methods | 送信を許可するHTTPメソッド |
| Access-Control-Allow-Headers | 送信を許可するHTTPヘッダ |
| Access-Control-Allow-Credentials | クッキーなど送信して良いか |
| Access-Control-Expose-Headers | JavaScriptからの参照を許可する |
| Access-Control-Max-Age | プリフライトリクエストの結果を保存していい期間 |
3.APIのリクエスト
レスポンスで送信許可を確認したブラウザは、APIのリクエストを送信します。
実処理でCORSを取り入れる
上記フロー「 2.リクエストの判断を行い、レスポンスの返却」の実装を追加することにより解決できました。
import express from "express";
import { generatePortfolioFile } from "./scripts/savePortfolio.ts";
const app = express();
const port = 3000;
+ app.use((req, res, next) => {
+ res.header("Access-Control-Allow-Origin", "http://localhost:5173"); // http://localhost:5173のオリジンを許可
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); // 送信を許可するHTTPメソッド
+ res.header(
+ "Access-Control-Allow-Headers",
+ "Origin, X-Requested-With, Content-Type, Accept, Authorization"
+ ); // 送信を許可するHTTPヘッダ
+
+ if (req.method === "OPTIONS") {
+ res.sendStatus(200);
+ } else {
+ next();
+ }
+ });
app.post("/api/generate-portfolio", async (req, res) => {
const { qiitaId, githubId } = req.body;
try {
await generatePortfolioFile(qiitaId, githubId);
res.status(200).send("Portfolio generated successfully");
} catch (error) {
res.status(500).send("Failed to generate portfolio");
}
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
corsパッケージを使うとより簡単に定義できます。
import express from "express";
import { generatePortfolioFile } from "./scripts/savePortfolio.ts";
import cors from "cors";
const app = express();
const port = 3000;
app.use(
cors({
origin: "http://localhost:5173", // http://localhost:5173のオリジンを許可
})
);
app.post("/api/generate-portfolio", async (req, res) => {
const { qiitaId, githubId } = req.body;
try {
await generatePortfolioFile(qiitaId, githubId);
res.status(200).send("Portfolio generated successfully");
} catch (error) {
res.status(500).send("Failed to generate portfolio");
}
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
おわりに
エラーを通してCORSの仕組みを学ぶことができました。
技術書を読んだだけでは理解しづらい概念もエラーを通して学ぶと理解度が上がると感じた出来事でした。
参考