誰もちゃんと教えてくれないDjangoのCORS、プリフライト対応(Vue CLI+axios+DRFのRestAPI通信エラー)

こういう泥臭い内容ってQiitaに書いたほうがいいのかな~と思いつつ。

とあるサンプルアプリを作っている中で、WebAPIの通信エラーに半日ハマりました。。。

 (とあるアプリの内容は本記事最下部のリンクにて)

  1. axios周りのコードをググってよくヒットする情報のまま書いてもうまく動いてくれない。
  2. DRFで作成したWebAPIにaxiosからアクセスするとCORS関連のエラーが出る。

ネットで調べても断片的な情報しか得られなかったのでまとめておきます。原因は複合していましたが、大きく分けると2点ほどです。 同じようなことで詰まっている方はお試しあれ。

環境

フロントエンド:

  • Vue CLI 4.3.0
  • axios 0.19.2

バックエンド:

いずれもOSはUbuntu 18.04 LTS、各々VirtualBox仮想マシンで構成し、同一ネットワーク上で動作させています。 IPアドレスとポート番号が異なる、つまりオリジンが違います。

1. axios周りのコードがうまく動いてくれない件

こちらは比較的短時間で解決。 ネット上でよく紹介されている内容は以下のような流れですが、4.のところでaxiosがうまく動いてくれません。(自分のvue-axiosの理解が足りないのかもしれませんが。)

  1. axiosとvue-axiosをインストール
  2. main.jsにaxiosとvue-axiosをインポート
  3. main.jsVue.use(axios,VueAxios) を追記
  4. データ操作(GET等)する処理のところでaxios.get()

以下のように修正するとちゃんと動いてくれます。

3.の代わりに main.js に以下を記述(2.のvue-axiosインポートの記述は削除)

Vue.prototype.$axios = axios

4.で実際にaxiosを使う際は以下のように記述

this.$axios.get()

この内容は公式にも記載されていました。Vueのインスタンスプロパティを利用するとのことです。$以降の文字列は何でもよさそうです。 jp.vuejs.org

あと、参考にさせて頂いたこちらのサイトでは正確かつ詳細に書かれています。 noumenon-th.net

2. DRFで作成したAPIにアクセスするとCORS関連のエラーが出る件

こちらはどハマりしました。 いわゆるCORS対応は済ませたつもりでしたが、足りなかったんですね。足りないと気付くまでに実施していた内容は次の通りです。

フロントエンド側(Vue)

main.js に以下を記載。

ちなみに、'Access-Control-Allow-Origin'はフロントエンド側の指定になりますが、ワイルドカード'*'はダメみたいです。

 axios.defaults.baseURL = 'http://x.x.x.x:nnnn' //バックエンド側のIPとポート
 axios.defaults.headers.common['Accept'] = 'application/json'
 axios.defaults.headers.common['Content-Type'] = 'application/json;charset=utf-8'
 axios.defaults.headers.common['Access-Control-Allow-Origin'] = 'http://y.y.y.y:mmmm'
 axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'

バックエンド側(Django

django-cors-headersをインストールして、setting.py に以下を記載。

  • INSTALLED_APPS に 'corsheaders' を追記
  • MIDDLEWAREに 'django.contrib.sessions.middleware.SessionMiddleware' を追記   ('corsheaders.middleware.CorsMiddleware'より上に記載する必要ありとのこと)
  • CORS_ORIGIN_WHITELIST を追加し、フロントエンド側のIPアドレス、ポート番号を記載

ネット検索するとよく出てくる、CORS_ORIGIN_ALLOW_ALL = Trueの記載は、ホワイトリストを明記していれば多分不要です。

INSTALLED_APPS = [
    'corsheaders', #追加
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', #追加
    'django.middleware.common.CommonMiddleware',

]

#追加
CORS_ORIGIN_WHITELIST = (
    'http://y.y.y.y:mmmm',
)

しかしながら、このままではVue+axiosで作成したページを開いた瞬間にプリフライト要求(preflight response)が発生し、その通信がエラーになります。ブラウザ画面上では、さも何も起こっていないかのようにみえ、画面上からは気づけません。その後、this.$axios.get()の処理に到達してもNetwork Errorが発生します。

プリフライト要求発生時にバックエンド側のログには

 "OPTIONS / HTTP/1.1" 200

と出ているので、ブラウザ側でプリフライト要求に対する応答が拒否されていると判断。FireFoxの開発者ツールで(←ここ重要)出力されているメッセージをみると、 「access-control-allow-headersにaccess-control-allow-originが含まれていません」 といった旨の内容が(詳細失念)。 え?と思い、応答側のHTTPヘッダをみてみると確かに、access-control-allow-headersには"accept"や"content-type"等は入っていましたが、"access-control-allow-origin"が入っていないことを確認。 じゃあ、Djangoのsetting.pyに何かしら記載して応答ヘッダに"access-control-allow-origin"を入れることができれば何とかなるではと予想し、再度ググるとそれっぽいのが出てきた。(ドンピシャの情報はなかったです)

バックエンド側(Django)への追加設定

setting.py にCORS_ALLOW_HEADERSを追記

CORS_ALLOW_HEADERS = (
 'accept',
 'accept-encoding',
 'authorization',
 'content-type',
 'dnt',
 'origin',
 'user-agent',
 'x-csrftoken',
 'x-requested-with',
 'access-control-allow-origin',
)

ポイントは'access-control-allow-origin',の箇所です。これでプリフライト要求も、そのあとのGETなども正常に通ります。よかったよかった。なお、一度プリフライト要求がOKになるとしばらくの間は有効のようです。

こちらのサイトの内容がヒントになりました。ありがとうございます。 teratail.com ja.coder.work tkkm.tokyo

余談

ちなみに、プリフライト要求が発生するのは「通常の通信である条件」から外れるもののようです。RestやWebAPIでは必ず発生しますので、オリジンがフロント/バックで別れていてDjangoを使っている場合はこの記事の対応は必須だと思います。ネット検索でよくヒットする対応方法としてはProxyを使った回避方法が紹介されていますが、そんなことする必要な場合があるのでしょうかね?

あと、上記参考サイトにも書かれていましたが、HTTP関連の不具合調査をChromeの開発者ツールで実施しないほうがよいです。解析に必要となる情報が表示されないことがあります。  

「とあるサンプルアプリ」について

興味のある方は以下をご覧ください。 handatec.hatenablog.com

今回はここまで。