【Laravel】Laravel デフォルトの JavaScript ファイルを拡張して外部の API を叩くと CORS 関連のエラーが起きる

 Laravel は PHP のフレームワークであり、Laravel が最も多い使用目的は web サイト、web アプリケーションの作成です。そのためブラウザ側のプログラミング言語である JavaScript のファイルも Laravel がデフォルトを用意しています。
Laravel
 このデフォルトの JavaScript ファイルには思わぬ挙動を引き起こすものが含まれています(v8.5.15で確認)。問題となるのは

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

 というコードです。これが実行されると JavaScript の HTTP 通信ライブラリである axios がデフォルトで HTTP リクエストヘッダに ‘X-Requested-With’ というキーで ‘XMLHttpRequest’ という値を埋め込む様になります。こうなると次のようなエラーが起きる時が現れます。


Access to XMLHttpRequest at ‘https://cpl.example.com/api/some’ from origin ‘http://cpl.docker-host.local:89’ 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.

 これの直接的な原因は resouces/js/bootstrap.js に含まれる

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

 というコードで、これを削除ないしコメントアウトすることで外部 API との通信を妨げられることがなくなります。また、適宜通信を通したいのであれば、次の様に外部 API との連携時のみ該当のヘッダを削除する方法が考えられます。

  const old = axios.defaults.headers.common['X-Requested-With']
  delete axios.defaults.headers.common['X-Requested-With'];
  axios
    .get(url)
    .then((data) => {
      console.log('data', data);
    })
    .catch((e) => {
      console.log('error', e);
    })
    .finally(() => {
      // 以前の設定に戻す
      axios.defaults.headers.common['X-Requested-With'] = old;
    });

 これが起きる原因は MDN のプリフライトリクエストが詳しいです。
Preflight request (プリフライトリクエスト) – MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
オリジン間リソース共有 (CORS) – HTTP | MDN#プリフライトリクエスト
 ざっくばらんにいえば、サーバ的に信用できないかもしれないリクエストをクライアントが投げる前に、あらかじめサーバにこのリクエストを投げて良いか尋ねる仕組みです。このプリフライトリクエストでサーバに処理を拒否されるとブラウザは CORS に関するエラーを出力します。このエラーは開発者ツールのネットワークタブで見るとプリフライトリクエストのエラーであることがわかりやすいです。次の画像は Firefox で見た場合で OPTIONS リクエストが GET リクエストの前に飛んでおり、この OPTIONS リクエストで CORS の適切な Allow Origin がなかった(ヘッダ情報的に通信が許されなかった)というエラーがでています。

次の画像らは Google Chrome で見た場合で、preflight(プリフライト)リクエストで失敗していること、プリフライトリクエストが OPTIONS メソッドであることがわかります。

 正常に動作する際は、このプリフライトリクエストが現れなくなります。

 地理情報の検索 API 等のウェブ上で公開されている認証情報なしで動作する API ではこのプリフライトリクエストのやり取りが実装されていない、あるいはプリフライトリクエストが必要ならばリクエストに対して常に拒否を返す、という API が多いです。これはプリフライトリクエストが不要な場合が定められており、多くの認証情報抜きで使用可能な API の正常な利用方法はプリフライトリクエストが不要な範囲におさまっているためです(つまり Laravel デフォルト JavaScript ファイルの axios の改造こそ web 上では例外)。

 プリフライトリクエストが不要となる場合は次のページにまとめられています。
オリジン間リソース共有 (CORS) – HTTP | MDN#単純リクエスト
 引用すると次です。

 ざっくばらんに言えば form タグ、 script タグ、 link タグの様な HTML タグで飛ばせるリクエストの範囲です。
 よくある認証情報抜きで外部公開されている API の場合、何かをキーに何かを取得するという機能さえあれば十分なことが多いです。そのような API は GET メソッドによるやりとりのみで済むためプリフライトリクエストを気にする必要がありません。プリフライトリクエストを受け取ったらエラーなり拒否なりを出力するだけです。
 問題となった

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

 の場合、単純リクエストとして許されないヘッダーである’X-Requested-With’を設定したため、コードとしてはシンプルな GET リクエストを送信する処理であってもプリフライトリクエストが飛ぶ運びとなります。このため API 側は余分な情報を送ってきた怪しい相手としてクライアント側を弾き、エラーが発生します。

 ちなみに

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

 という記述がある理由ですが、これは非同期通信である XHR(ajax や axios のラップ元)ということを示すためにあります。Laravel のベースとなっている PHP フレームワークである Symfony の内部にも次のコードが含まれています。

// symfony/http-foundation v4.4.18
// \Symfony\Component\HttpFoundation\Request::isXmlHttpRequest 

    /**
     * Returns true if the request is a XMLHttpRequest.
     *
     * It works if your JavaScript library sets an X-Requested-With HTTP header.
     * It is known to work with common JavaScript frameworks:
     *
     * @see https://wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript
     *
     * @return bool true if the request is an XMLHttpRequest, false otherwise
     */
    public function isXmlHttpRequest()
    {
        return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
    }

 結局のところ、これは必要に応じて使い分けるべきものです。私的なおすすめは bootstrap.js の該当コードを削除し、外部 API 通信用の axios インスタンスを生成する関数と Laravel を入れておくサーバと通信する用の axios インスタンスを生成する関数を用意して適宜使い分ける方法です。

/** Laravel と通信するための axios インスタンスを生成するコード */
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

export const createAxiosInstance = (): AxiosInstance => {
  const baseConfig: AxiosRequestConfig = {
    baseURL: '/api/',// ルート相対パスで URL を指定することを想定
    headers: {
      Accept: 'application/json', // JSON レスポンスを返して欲しいことを明示
      'X-Requested-With': 'XMLHttpRequest', // axios を使っていることを明示
      // ↑二つとも Laravel と通信する際にはプラスに働きやすい設定です
    },
  };
  // 他にも色々 axios 用の設定を詰め込む

  return axios.create(baseConfig);
};
// 使用例
createAxiosInstance()
  .get('member') // ${APP_URL}/api/member に GET リクエスト
  .then(/** なんやかんや */)


/** Laravel と通信するための axios インスタンスを生成するコード */
export const createAxiosInstanceForOuter = (): AxiosInstance => {
  const baseConfig: AxiosRequestConfig = {
    // URL はフルパス指定を想定
    // 単純リクエストを妨げるヘッダ定義はなし
  };
  // 他にも色々 axios 用の設定を詰め込む

  return axios.create(baseConfig);
};

// 使用例
createAxiosInstanceForOuter()
  .get('http://api.example.com/pref-names') // 都道府県名を返す外部 API に GET リクエスト
  .then(/** なんやかんや */)
>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG