はじめに
表題の通り、React から FuelPHP API をコールしようとしたのですが、思いもよらず苦戦したのでその顛末をまとめておこうと思いました。
CORS 自体はもともと設定して使っていたので理解していたつもりだったのですが、Basic認証が絡むことにより複雑化することを理解できていなかったのが一番の敗因でした。
そして、それとあわせて Apache 回りの設定の理解度が浅かったことがトドメを刺した感じです。
一般化するのが面倒なので React, FuelPHP を例としたメモにしますが、基本的には JavaScript が Apache ベースの API をコールする際に起き得る内容として読み替えることが可能だと思います。
やりたいこと
React 製の Web アプリケーション内で、下記のように url を指定して API コールし、レスポンスを JSON として受け取る。
fetch(url).then((res) => res.json());
url で指定される API のエンドポイントは以下のような状態となっている。
- Web サーバは Apache-2.4 系
- url のドメインは React 製の Web アプリケーションが動いているドメイン(localhost:8888 とする)とは別のドメイン(yyyy.zzzz とする)
- 上記のドメインは FuelPHP ですべて実装され動作している
- url の path は
/admin/xxx
のように/admin
の配下のエンドポイントで、Basic 認証が必要 - Basic 認証は、予めブラウザの別タブなどで該当 URL を参照して認証させることで、React 内でBasic 認証自体を解決する必要はない
解決すべきこと
以降で、解決すべきことを順番に挙げていきます。
CORS 設定
まず、「url のドメインは React 製の Web アプリケーションが動いているドメインとは別のドメイン」の理由により、CORS の設定が必要です。
CORS の詳細については、いろいろなところで解説されているので省きます(^_^;
ここでは対応内容のみを記載します。
React 側では下記のように CORS を適用することを指定します。
fetch(url, {
mode: "cors",
}).then((res) => res.json());
API 側では CORS に関するヘッダをレスポンスとして返すよう、下記のように Apache で設定します。
なお、設定は httpd.conf でも .htaccess でも基本的には大丈夫です。
(後述しますが、今回の条件の場合ではこれがNGだったのが問題の1つです)
すでに、htpd.conf の <Location "/admin">
ディレクティブで /admin 以下の設定を行っていたので、今回はこのディレクティブ内に記載することにしました。
ワイルドカード指定は本来は限定して指定した方が良いですが、とりあえずは API コールできる設定を目指すので最大限の許可としました。
Header set Access-Control-Allow-Origin "*"
Header set Access-Control-Allow-Headers "*"
Header set Access-Control-Allow-Methods "OPTIONS, GET, POST, PUT, DELETE"
これで大丈夫かと思いきや、React の fetch() ではエラーとなりました。
また、curl で下記のように確認してみたところ、設定したはずのヘッダがまったく返ってきていませんでした。
% curl -s -D - -o /dev/null --user adminuser:adminpass https://yyyy.zzzz/admin/xxx
ちなみに、同じような設定を /admin/.htaccess で設定してみても同様でした。
credentials を有効にする
これはおかしい!と思って、試しに Virtualhost のトップレベルで同じ設定をしてみると、curl コマンドではヘッダが正しくセットされて返ってきました。
ということは、なぜか htpd.conf の <Location "/admin">
ディレクティブや .htaccess ではヘッダのセットができないということになりますね。。
上記は上記で問題なのですがいったん置いておいて、とりあえずヘッダがセットできたので改めて React fetch() の挙動を確認してみたのですが、変わらず下記のエラーでした。
Access to fetch at 'https://yyyy.zzzz/admin/xxx' from origin 'http://localhost:8888' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
ここではじめて Basic 認証の影響を疑いました。
試しに Basic 認証を外してみると、正しく動く!!
ということで、Basic 認証が絡むと通常の CORS の設定では動かないことが判明。
調べてみたところ、まず、React 側では下記のように credentials を設定する必要があります。
fetch(url, {
mode: "cors",
credentials: "include",
}).then((res) => res.json());
そして、Apache 側でも下記のように credentials を許可するようレスポンスヘッダを追加します。
Header set Access-Control-Allow-Credentials: true
ちなみに、credentials は Cookie を送りたい時にも指定する必要があるようです。
上記を設定した結果、、、React の fetch() でのエラーは下記のようなエラーに変わりました(/_\*)
Access to fetch at 'https://yyyy.zzzz/admin/xxx' from origin 'http://localhost:8888' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
どうやら、credentials を有効とする場合にはワイルドカード指定は使えないようです。
ワイルドカード指定をやめる
いま、ワイルドカードは2カ所で使っているのでそれぞれ調べてみました。
まずは、Access-Control-Allow-Origin
について。
こちらは簡単ですね。面倒くさがらずに下記のように許可したいドメインを明記します。
Header set Access-Control-Allow-Origin http://localhost:8888
次に、Access-Control-Allow-Headers
。
こちらはワイルドカードを使ってもOKなのでそのままでも良さそうだったのですが、念のため下記のように Authorization
を追加しておきました。Authorization
はワイルドカード指定では含まれないようなので。
Header set Access-Control-Allow-Headers '*,Authorization'
上記の設定で、無事 React fetch() が正常に動作しました!!
API側のレスポンスヘッダの設定箇所の改善
とりあえず上記までで目的の動作は達成できたのですが、最初の方で問題を置いておいた「なぜか htpd.conf の <Location "/admin">
ディレクティブや .htaccess ではヘッダのセットができない」件について解消します。
この現象ですが、<Location "/admin">
ディレクティブ、 .htaccess でいずれも Basic 認証の設定は有効に機能していたのですが、Header set
は機能しないという状態でした。
原因は結論から言うと、この API のサイトではサイト全体で FuelPHP を使っているため、/admin/xxx なリクエストは /index.php?/admin/xxx へ RewriteRule で書き換えられます。
どうやら、httpd.conf の Location ディレクティブはこの RewriteRule で書き換えられた後で適用されるようで、そのため <Location "/admin">
では適用されないということになったようです。
.htaccess も同じ理由で、Header set が適用されなかったようです。
Basic 認証の方はどちらもしっかり適用されていたので、これはかなり紛らわしいですね〜
ということで解決方法は RewriteRule 後の path を使って設定すれば良いと思いますが、自分としてはどうもこの Apache の設定は納得がいかなかったので、結局 FuelPHP 側に手を入れて解決させました。
API のベースクラス的なものの after メソッドに下記のようにレスポンスヘッダを追加するように書きました。
public function after($response) {
$response = parent::after($response);
$response->set_header('Access-Control-Allow-Origin', 'http://localhost:8888');
$response->set_header('Access-Control-Allow-Headers', '*,Authorization');
$response->set_header('Access-Control-Allow-Methods', 'OPTIONS, GET, POST, PUT, DELETE');
$response->set_header('Access-Control-Allow-Credentials', 'true');
return $response;
}
コメント