1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

レビュー送信が「CORSエラー」で落ちる。犯人はCORSじゃなく、しかも2人いました

1
Last updated at Posted at 2026-06-10

はじめに

Spring Boot + React で小さなレビュー投稿アプリを作って勉強しています。

「レビューを送信」ボタンを実装して押したら、コンソールが真っ赤に。しかも CORS policy の文字。「またCORSか…」と直し始めたら、犯人はCORSではなく、姿の違うやつが2人いました。同じ仮面をかぶって二度だましてくる——まんまと化かされました

エラーメッセージを額面どおり受け取ると迷子になる、という記録です。

環境

  • Java 21 / Spring Boot 3.5.14
  • React + Vite(フロント localhost:5173 / API localhost:8080
  • 認証:ログインで JWT を発行 → 各リクエストに Authorization: Bearer <token> を付ける方式

起きたこと

レビュー欄に文字を入れて送信すると、画面は無反応。コンソールにはこれ:

Access to fetch at 'http://localhost:8080/posts/8/reviews' from origin
'http://localhost:5173' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Failed to load resource: net::ERR_FAILED

おかしいのは、同じアプリの「投稿を作成」は、同じ localhost:8080 宛てでちゃんと動いていること。CORS設定がダメなら投稿作成も落ちるはずなのに……。

ちなみにこのリクエストはサーバーには届いていて、403が返っています。ただしブラウザが「CORS的にダメ」とJS側にレスポンスを渡さないので、フロントからは本当のステータスが見えにくいのです(サーバーのログを見ると403が記録されています)。

原因①:動く双子と壊れた双子を比べる

「動く投稿作成」と「動かないレビュー送信」はやることがそっくり。なら並べて違いを探せばいい。キーワードで検索しました。

grep -n "Authorization" src/App.jsx
60:        'Authorization': 'Bearer ' + token,   ← 投稿作成
98:      headers: { 'Authorization': 'Bearer ' + token },  ← 削除

レビュー送信の関数がこの結果に出てこない。つまり「動く方には有り、レビュー送信だけ無い」。トークンを付け忘れていました。

    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + token,   // ← この1行が抜けていた
    },

なぜ「認証なし」が「CORSエラー」に化けるのか

レビュー投稿はログイン必須。トークン無しでPOSTすると、サーバーは拒否します。ポイントは Spring Security がコントローラより手前のフィルタで弾くこと。手前で弾かれたレスポンスには Access-Control-Allow-Origin が付かない → ブラウザは「CORS的にダメ」と判断し、本当のエラーを隠して CORSエラーとして表示します。

今回の本体は、認証情報が足りないことによる 403(設定によっては401になる場合もあります)。CORSエラーはその症状。

原因②:直したのに、まだ落ちる

Authorization を足して再送信。まだ net::ERR_FAILED。Networkタブでヘッダーを確認すると、今度はちゃんと付いている:

authorization: Bearer eyJhbGci...(中略・トークンは長いので省略)...XYZ

ヘッダーはある。じゃあ中身は?

※ここではローカル学習用のトークンで確認しています。実トークンを jwt.io などの外部サイトに貼るのは避けましょう。

JWTのペイロードをデコードすると:

{ "sub": "myuser", "iat": 1781049468, "exp": 1781053068 }

exp(有効期限)が現在時刻より前=期限切れ。このアプリのJWTは発行から1時間で失効する設定(JwtUtil1000 * 60 * 60 ミリ秒)。ログインからだいぶ経っていたのです。

サーバーが正常かは curl で切り分け:

# 新しくログインして取り直したトークンでPOST → 200なら、サーバーもコードも正常
curl -s -o /dev/null -w "%{http_code}\n" \
  -X POST http://localhost:8080/posts/8/reviews \
  -H "Authorization: Bearer <新しいトークン>" \
  -H "Content-Type: application/json" \
  -d '{"content":"テスト"}'
# => 200

200。バックエンドは無実で、フロントが古いトークンを握りっぱなしでした。画面が「ログイン中」のままだったのは、フロントが if (!token) で文字列の有無しか見ていないから。中身が期限切れかは、サーバーに弾かれて初めてわかります。

解決

  1. レビュー送信のfetchに Authorization: Bearer ' + token を足す
  2. ログインし直して新しいトークンを取り直す

結局、①Authorization ヘッダーの追加 + ②ログインし直し(新しいトークン取得)の2つで直りました。"犯人が2人" だったので、対処も2つです。

なお今回は学習用なのでログインし直しで対応しました。実務では、401/403を受け取ったらトークンを破棄してログイン画面へ戻す、といった処理が必要です(今後の課題)。

学び

  • DevToolsの「CORSエラー」は、本当の原因の影のことがある(特に Spring Security + 認証付きAPI)
  • 認証付きPOSTが弾かれたら、まず Network で Authorization の有無を見る
  • 「動く機能」と「動かない機能」を grep で並べて比較すると、"無いもの"が一発で見える
  • 直らないときは curl でサーバー単体を試し、フロント/バックを切り分ける

おわりに

エラーメッセージって、必ずしも本当の原因を指してくれないんだな…と痛感しました。
同じ「CORSじゃないのにCORSと言われる」で固まった人の参考になれば嬉しいです🙌

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?