【入門編】 CORS(Cross-Origin Resource Sharing)のプリフライトリクエスト(OPTIONSメソッド) – Web APIアーキテクチャ・データ連携実践ガイド

こんにちは!インフラアーキテクトの私です。日々、ネットワークの海を漂うパケットを見つめながら、「どうすればシステムがもっと美しく、スムーズに動くだろうか」と妄想する毎日を送っています。

さて、Webアプリケーションを作っていると、一度は必ずぶつかる壁がありますよね。「あれっ、フロントエンドからAPIを叩いたのに、なぜかブラウザに怒られてデータが取れないぞ……?」というあの現象です。そう、犯人は大体 CORS(Cross-Origin Resource Sharing) です。

今回は、そのCORSの裏側でこっそりと、しかし非常に重要な役割を果たしている 「プリフライトリクエスト(OPTIONSメソッド)」 について、ネットワークの郵便配達の物語にたとえながら、優しく紐解いていきたいと思います。難しい用語が出てきても「一歩ずつ理解していきましょう!」ね。それでは、出発進行です!

—

1. なぜブラウザは「お伺い(OPTIONS)」を立てるのか?

私たちが普段使っているWebブラウザは、実はものすごく「用心深い門番」を心の中に飼っています。これをセキュリティの世界では 同一生成元ポリシー(Same-Origin Policy) と呼びます。

簡単に言うと、「Aというお店(ドメイン)の敷地内で動いているスクリプトが、勝手に全く関係ないBというお店(別ドメイン)の金庫からデータを盗み出さないようにしよう!」という安全装置です。

郵便配達のたとえで考えてみよう

想像してみてください。あなたが「A町」の自宅にいます。そこから「B町」にあるカフェのポストへ、手紙を入れようとしています。

もし、B町のカフェの用心棒が「おい、どこの誰だ!見慣れない顔だな!」と、手紙の中身を見る前に門前払いを食らわせたらどうでしょう?あるいは、あなたが送りつけた危険な荷物(ブラウザのセキュリティを脅かすデータ)だったら、カフェの営業が台無しになってしまいますよね。

そこで、賢いブラウザ(郵便配達員)は、本命の大切な荷物をいきなり投げ込むのではなく、まずはカフェの店員さんに向かってこう尋ねます。

> 「すみません、A町の者なんですが、これからB町のこのカフェに『こういう荷物』を『こういう方法』で送りつけても、本当に大丈夫ですか?」

この「事前にお伺いを立てる確認の通信」こそが、HTTPの OPTIONS メソッド を使った プリフライトリクエスト(事前リクエスト) なんです!

—

2. プリフライトリクエストの裏側を覗いてみよう

では、ブラウザとサーバーの間で、実際にはどんなやり取りが行われているのでしょうか?パケットの動きを追ってみましょう。

ブラウザは、私たちがJavaScriptなどで fetch() や axios を使って別ドメインのAPIを叩いたとき、いきなり本番の POST や PUT リクエストを送るのではなく、その直前にこっそりと以下のような OPTIONS リクエストを飛ばしています。

ブラウザが送る「お伺い」の正体(OPTIONSリクエストの例)

OPTIONS /api/v1/posts HTTP/1.1
Host: api.example.com
Origin: https://my-frontend.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

ここで登場する重要なキーワードを、そっと整理しておきましょう。

  • Origin: 「今、私はこのお家(ドメイン)から来ました!」という差出人の住所です。
  • Access-Control-Request-Method: 「これから本番で POST メソッドを使いたいんだけど、いいかな?」という事前申請です。
  • Access-Control-Request-Headers: 「本番の通信では、Content-Type や Authorization(認証トークン)といった特別なヘッダーを使いたいんだけど、許可してもらえる?」という詳細なお願いです。

サーバーからの「お返事」はどうなる?

この OPTIONS リクエストを受け取ったサーバー側は、インフラエンジニアが適切に設定したルール(CORS設定)に基づき、以下のような返事を送り返します。

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://my-frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

サーバーが「よし、君のところ(https://my-frontend.com)からの POST や指定されたヘッダーなら受け付けてあげるよ!」と許可(Access-Control-Allow-Origin など)を返して初めて、ブラウザは安心の太鼓判を押して、本来の目的である POST リクエスト(本番の通信)を送り出すのです。

—

3. 実務でよくある罠と、サーバー側の正しい設定方法

「仕組みはわかったけれど、自分の開発環境や本番環境で、なぜかCORSのエラーが出てしまう……」
そんなときは、大体サーバー側の OPTIONS に対する応答(レスポンスヘッダー)が漏れています。

ここでは、実務でよく使われる代表的なWebサーバー(Nginx)と、バックエンド(Node.js / Express)での具体的な設定例を見ていきましょう。そのままコピペして、コメントを参考に調整してみてくださいね。

パターンA:Nginxでリバースプロキシを組む場合

フロントエンドとバックエンドのドメインが異なり、NginxでCORSのヘッダーを付与する場合の設定例です。

server {
    listen 80;
    server_name api.example.com;

    location /api/ {
        # 1. プリフライトリクエスト(OPTIONS)が飛んできた場合の特別な処理
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://my-frontend.com' always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
            
            # プレフライトの結果をキャッシュさせる時間(秒単位: 86400秒 = 24時間)
            # これにより、毎回OPTIONSを飛ばさなくてよくなるため、通信が高速化します!
            add_header 'Access-Control-Max-Age' 86400;
            
            # 中身のない成功レスポンス(204)を即座に返す
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        # 2. 通常のリクエストに対するCORSヘッダーの付与
        add_header 'Access-Control-Allow-Origin' 'https://my-frontend.com' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;

        # バックエンドのアプリケーションへ転送
        proxy_pass http://127.0.0.1:3000;
    }
}

パターンB:Node.js (Express) を使う場合

もっと手軽にコード側で制御したい場合は、公式の cors ミドルウェアを使うのが一番安全でスマートです。

const express = require('express');
const cors = require('cors');
const app = express();

// CORSの詳細な設定を定義します
const corsOptions = {
  // 許可するフロントエンドのオリジンを指定
  origin: 'https://my-frontend.com',
  // 許可するHTTPメソッドを指定
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  // 許可するリクエストヘッダーを指定
  allowedHeaders: ['Content-Type', 'Authorization'],
  // プリフライトリクエストの成功ステータス(一部の古いブラウザ対策として204を指定)
  optionsSuccessStatus: 204 
};

// アプリケーション全体にCORSミドルウェアを適用
app.use(cors(corsOptions));

app.post('/api/v1/posts', (req, res) => {
  res.json({ message: 'CORSを突破してデータが正常に届きました!' });
});

app.listen(3000, () => {
  console.log('サーバーがポート3000で元気に稼働中です!');
});

—

4. まとめ:プリフライトは「安全のための優しいひと手間」

ここまで、CORSのプリフライトリクエスト(OPTIONS メソッド)の挙動について見てきました。

一見すると、「本番通信の前にわざわざ1回通信が増えるなんて、パフォーマンスの邪魔だなあ」と感じるかもしれません。しかしこの OPTIONS による事前の確認があるおかげで、私たちのブラウザやWebアプリケーションは、悪意あるクロスサイト攻撃からしっかりと守られています。

もし今後、開発中に「CORSエラーだ!」と遭遇したときは、焦らずにこう考えてみてください。
「あ、今ブラウザという名の用心棒が、サーバーに対して『このリクエスト通していい?』ってお伺いを立てているけれど、サーバー側がお返事(ヘッダー)の仕方を忘れちゃっているんだな」 と。

インフラやネットワークの仕組みが分かると、エラー画面すらも「おっ、今はここでパケットがこう会話しているんだな」と愛おしく(?)思えてくるはずです。ぜひ今回の設定例も参考に、ご自身の環境のCORS周りを見直してみてくださいね。

それでは、また次回の深淵なるネットワークの世界でお会いしましょう!

コメント

タイトルとURLをコピーしました