【Express + Vue.js】Web APIでSPAを実装するために学ぶべきHTTPヘッダの知識をまとめる

投稿日:

プログラミング初心者の勉強ブログ #135

SPA構築にあたって、クライアント側をVue、サーバー側をExpressでそれぞれ実装していこうと思い立ちゴリゴリやってみると、APIの通信でなんか色々詰まってしまった。理由はHTTPヘッダの設定(特にサーバー側)がよくわからなかったためである。開発環境のため今後もまだエラーが出る可能性はあるが、色々調べて腑に落ちた部分があるので、そこらへんをまとめていく。

 

目次

 

経緯

Vue.js + ExpressでのSPA開発にあたり、Vueで実装するクライアント側(以下クライアント側)からのaxiosで、Expressで実装するサーバー側(以下サーバー側)のAPIを叩きたく、コードを書いてみたものの「Response to preflight request doesn't pass access control check: It does not have HTTP ok status.」エラーによって結構な時間詰まってしまった。

実装内容としてはFacebookログインをJWTで認証していくもの。他にもエラーが出たりしたし、そもそも理解が曖昧すぎたので、とりあえずHTTPヘッダについて1から勉強し直した。

 

HTTPヘッダ問題解決のために

APIでリクエスト/レスポンスを通すためにHTTP注目したポイントをまとめていく。これ知っとかないとやばくないか?的な内容なのに、全然知らないことばかり出てきて焦ったのが正直な感想。

 

そもそもブラウザからは、そのWebページと同ドメイン宛にしかHTTPリクエストを送れない

JSのajaxやfetch、axiosなどで実装した、ブラウザで動く非同期HTTP通信は、同一オリジンポリシーと呼ばれる決まりによって、そのWebページの生成元のドメインにしかリクエストを送ることができない。この仕組みからそもそも僕はわかっていなかった。

参考:同一オリジンポリシー - Web セキュリティ | MDN

 

解決するには、XMLHttpRequestを使うか、JSONPで制限から逃れるかの2パターン

同一オリジンポリシーによって、違うドメイン間でのHTTPリクエストは制限される。この問題を解決するためには、ブラウザでXMLHttpRequestオブジェクトを使用するか、JSONPを使うかのどちらかの対応が必要になる。推奨はXMLHttpRequest。JSONPは制限をかいくぐる手法のため、セキュアな設計ではなくなり悪意のあるユーザーからの攻撃を受けるリスクが高まる。(このように書いてはいるが、僕はGASでスプレッドシートを利用したときのajaxでJSONPをよくわからず使っていたのを思い出す。)

 

クロスオリジンでのリソース共有をするためには、「CORS」という決まりを守らなければならない

クロスオリジン(違うドメイン)間でのリソース共有とは、今回のようなSPAでクライアント側とサーバー側を別ドメインで作成し、APIを叩く場合などに生じる。(今回の場合クライアント側はlocalhost:8080、サーバー側はlocalhost:3000。)

CORSとは「クロスオリジンリソースシェアリング」の略であり、このようなクロスオリジン間でのリソース共有をする場合の決まりを定めている。

Chrome検証ツールで

「Access to XMLHttpRequest at 'サーバー側ドメイン' from origin 'クライアント側ドメイン' has been blocked by CORS policy」

とエラーが出るときは、このCORSの決まりを守っていないから発生していると考える。

具体的には、

  • クライアント側での、XMLHttpRequestによるリクエストであることの宣言(リクエストの種類が複数ある場合)
  • サーバー側での、クロスドメイン許可(クライアント側のドメインを許可する)
  • サーバー側での、HTTPヘッダの許可(カスタムヘッダなど)
  • サーバー側での、HTTPメソッドの許可(GETやPOSTなど)

これらの設定をクライアント側、サーバー側であらかじめ設定しておく必要がある。

 

ajaxやsuperagent、axios、fetchなどはXMLHttpRequest(XHR)のライブラリと考えるとわかりやすい

XMLHttpRequestの字面をみると、素のJavaScriptで非同期通信を書くときに使われているやつだな確か、くらいの印象しかなかったが、どうやらこのオブジェクトがajaxやaxiosなどの非同期通信の大元と考えるべきなのだと知った。ここら辺かなりアバウトな覚え方をしていたんだと感じた。

XMLHttpRequest (XHR) オブジェクトを使用すると、サーバーと対話することができます。ページ全体を更新する必要なしに、データを受け取ることができます。これでユーザーの作業を中断させることなく、ウェブページの一部を更新することができます。 XMLHttpRequest は AJAX プログラミングで頻繁に使用されます。

参考:XMLHttpRequest - Web API | MDN

 

Access-Control-Allow-Origin未設定によるエラー

まず、サーバー側でHTTPリクエストを許可するオリジン(ドメイン)を設定する必要がある。

この場合、http://localhost:8080のドメインを許可することになる。最初に詰まったのはここであった。これを調べていくにあたって、「Access-Control-Allow-Headers」「Access-Control-Allow-Methods」「Access-Control-Max-Age」あたりも出てきたので、設定した。

 

app.js (Express)

こんな感じ。

 

preflightリクエストの存在

リソースにアクセスするための通信は2つの仕組みが存在する。

  1. 通常のHTTPリクエストによるアクセス
  2. preflightリクエストで通信が可能かどうか確認したのちの、HTTPリクエストによるアクセス

つまり、最初っからいきなりHTTPリクエストを送らず、まずpreflightというリクエストを送り通信可否を確認する場合が存在するということ。僕はここでかなり詰まった。

クライアント側で「It does not have HTTP ok status.」というエラーが出ており、サーバー側を見ると、

「OPTIONS /api/v1/getCurrentUser 404 >0.210 ms - -
express_1  | Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client」

OPTIONSというメソッドのリクエストで404が帰ってきてしまう。GETで送っているのにOPTIONSでエラーが出ていることが謎すぎで途方に暮れていたが、どうやらOPTIONSによるリクエストがpreflightによるリクエストらしい。

app.js (Express)

 

最終的にサーバー側はこのようなコードで現状完結しているのだが、

 

ここの部分でOPTIONSリクエストに対してhttpOKステータスを返すように定義することで解決した。

 

ちなみにクライアント側は、

axiosの設定で、headerに対してContent-Typeと X-Requested-Withを定義した。

 

preflightリクエストになる条件

どういうときにpreflightリクエストになるか。

以下の条件の全てに該当する場合、ブラウザはpreflightリクエストを送る必要がないと判断し、シンプルなリクエストを送信します。それ以外の場合は、preflightリクエストを送信します。

  • HTTPメソッドがGET, POST, HEADのいずれか
  • HTTPヘッダにAccept, Accept-Language, Content-Language, Content-Type以外のフィールドが含まれない
  • Content-Typeの値はapplication/x-www-form-urlencoded, multipart/form-data, text/plainのいずれか

CORS(Cross-Origin Resource Sharing)について整理してみた | DevelopersIO

この記事を読んでものすごくスッキリした。HTTPヘッダでお悩みの方は読んだ方が良い。

 

まとめ

CORSについてやっと理解が進んだ気がする。OPTIONSメソッドとかどこで使われてるんだ?という疑問とか。ajaxやaxios、superagent、fetchなどはXHRオブジェクトを使いやすくしたライブラリ的なものであるという理解も僕の中ではしっくりきている(superagentとfetchは使ったことないけど)。APIは楽しいからクライアント側とサーバー側を分ける実装は今後もやりたい。裏をAPIにしておいて、いつになるかわからないがSwiftネイティブアプリと連携したい。

以上ありがとうございました。

 

-プログラミング学習
-, , , ,

Copyright© s u p ? , 2019 All Rights Reserved.