1. はじめに(構成と起きた問題)
個人開発で、競馬収支管理アプリ『Turfolio』というSingle Page Application(SPA)を開発しています。
今回の開発では、以下の技術スタックを採用しました。
-
フロントエンド: React (Vite) / Cloudflare Pages (
*.pages.dev) -
バックエンド: FastAPI / GCP Cloud Run (
*.run.app)
ここで実装したかったのは、「バックエンドで発行したセッションCookieを使ったシンプルなログイン認証」です。しかし、いざ本番環境で通信テストをしてみると、思わぬ問題に直面しました。
【起きた問題】
- ログインAPI(
POST)自体は200 OKで成功し、ブラウザのApplicationタブを見ると確かにCookieは保存されている - しかし、その直後に実行されるユーザー情報取得API(
GET)で、なぜか401 Unauthorizedになってしまう - 開発者ツールで確認すると、後続のリクエストのRequest Headersに
Cookieが含まれていない
本記事では、この問題の原因と、そこから導き出した最終的な解決策について共有します。同様の構成で開発される方の助けになれば幸いです。
2. 原因:ブラウザの「サードパーティCookieブロック」
当初、私はバックエンドのCookie発行処理やCORS設定のコードにミスがあるのだと考えていました。
バックエンド(FastAPI)で samesite="none" や secure=True を設定し、フロントエンド(axios)でも withCredentials: true を設定しており、コード上は問題ないはずでした。しかし、それでもCookieが送られません。
原因は、フロントエンドとバックエンドでドメインが異なる(クロスドメイン) ため、現代のブラウザ(Chrome、Safari、Brave等)の強力なプライバシー保護機能によって、通信実行時にCookieが強制的にブロックされていたことでした。
※試しにChromeの「サードパーティCookieを許可する」設定をオンにすると正常に通信できたことから、この原因を特定しました。
3. リバースプロキシでの解決と、失敗したアプローチ
【解決の方針】
コードの修正で戦うのをやめ、「フロントとバックを同じドメイン(同一オリジン)に見せかける」ために、Cloudflare側でリバースプロキシを組む方針をとりました。
❌ 失敗したアプローチ(_redirects)
最初に試したのは、Cloudflare Pagesの標準機能である public/_redirects ファイルを使ったプロキシ設定です。以下のように記述しました。
/api/* https://...run.app/api/:splat 200
これによって GET リクエストは無事、通るようになりましたが、ログアウトやデータ登録などの POST や PUT リクエストを投げると、今度は 405 Method Not Allowed が発生するようになってしまいました。
理由:
Cloudflare Pagesの _redirects によるリバースプロキシ(ステータスコード 200 でのリダイレクト)は、仕様上 GET と HEAD リクエストしか転送してくれないためです。POST や PUT が必須となるAPIのプロキシには使えませんでした。
4. 最終的な解決策:Pages Functionsによる完全なプロキシ
_redirects による転送を諦め、Cloudflare Pagesのサーバーレス機能である Cloudflare Pages Functions を使って本格的なプロキシを構築しました。
以下の2ステップで実装します。
① バックエンドへのプロキシ実装
フロントエンドのプロジェクト内に functions/api/[[path]].js を作成し、元のリクエスト情報(メソッド、ヘッダー、ボディ)をそのままCloud Runへ転送する処理を書きます。
export async function onRequest(context) {
const { request } = context;
const url = new URL(request.url);
// 1. 転送先のベースURLを指定し、パスとクエリを引き継いでURLを構築
const backendBaseUrl = 'https://プロジェクト名-xxxx.run.app';
const targetUrl = new URL(url.pathname + url.search, backendBaseUrl);
// 2. 元のHostヘッダーをコピーして削除(★重要!)
// これを消さないとCloud Run側でドメインミスマッチが起き、404 Not Foundになります
const proxyHeaders = new Headers(request.headers);
proxyHeaders.delete('Host');
proxyHeaders.delete('X-Forwarded-Host');
// 3. メソッド、ヘッダー、ボディを引き継いでプロキシリクエストを作成
const proxyRequest = new Request(targetUrl, {
method: request.method,
headers: proxyHeaders,
body: request.body, // POST/PUT等のリクエストボディをそのまま流す
redirect: 'manual' // リダイレクトはプロキシ側で追跡せずブラウザへ透過させる
});
return fetch(proxyRequest);
}
💡 ここがポイント!
-
Hostヘッダーの削除: 転送元(*.pages.dev)のHostヘッダーを削除してfetchを実行することで、Cloudflareが自動的に宛先であるCloud Runのホスト名を再付与してくれます。これを怠ると、Cloud Run側でリクエストが弾かれ404になるため必須の処理です -
bodyのパススルー:request.bodyをそのまま渡すことで、POST/PUT/DELETEのボディデータ(JSONなど)も問題なくバックエンドへ届きます
② フロントエンドの呼び出し元の修正
フロントエンドからAPIを呼び出す際の baseURL を、環境変数(Cloud Runの絶対URL)から、相対パスの /api に変更します。
import axios from 'axios';
const api = axios.create({
baseURL: '/api', // ← 相対パスにすることでブラウザには同一オリジンに見える!
withCredentials: true
});
export default api;
これにより、ブラウザからは同一ドメイン(自サーバー)への通信に見えるため、サードパーティCookieブロックを完全に回避しつつ、POSTやPUTも含めたすべての通信が透過的にバックエンドへ届くようになりました。
5. まとめ
今回の開発を通して、以下の知見を得ました。
- フロントとバックでドメインが分かれる構成では、現代のブラウザのサードパーティCookieブロックに要注意
-
samesite="none"や CORS設定といったコードレベルの調整だけでなく、インフラ層(プロキシ)で同一オリジンに揃えるのが現在の確実で安全なアプローチ - Cloudflare PagesでAPIをプロキシするなら、
_redirectsではなくfunctionsを使うこと(POST/PUTリクエストを正しく通すため)
SPA + APIサーバーという構成において、Cookie周りのエラーやCORSの問題に苦しんでいる方の参考になれば幸いです。
参考リンク
- 競馬収支管理アプリ Turfolio: アプリTop
- TWAとしてAndroid版をGoogle Playストアにも公開しています: PlayストアURL
- 機能紹介: note