GAS as Web App - XHRは使えるのか

# 発端

最近よくGoogle Apps Scriptの開発を行います。その中で頻度が増えているのが静的HTMLをGoogle Apps Scriptより返して簡易なWebホスティングを行うシーンです。当初はこんな構成を取りたいと考えました。

<img width="1100" alt="スクリーンショット 2020-08-10 13.48.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633687/59d5c2f4-9271-c0b5-1591-62aa38a5ef27.png">



# データを返すApp Scriptを分ける意味

開発効率が良いと思った為です。HTMLを返すApp Script上でも`google.script.run`を使えばHTML側からApp Scriptの関数を操作、データの取得が可能です。しかし幾つか開発をしていく中で以下のような不満にあたりました。

- 開発・検証時に一々App Scriptにデプロイしなければ検証出来ない
- 他プラットフォームへの転用時にコードの修正箇所が多い
- 複数のApp Scriptから1つのシートを利用したい時、ファイルの管理が煩雑



これがクライアントサイドから直接別のApp Scriptよりデータを取得する仕組みが取れれば大変楽ができる、と思った次第です。

# 結論

先にどうなったか記載致します。最終的に限定的には出来る、でした。

1. クライアントサイドからFetch or XHRで認証のかかっていないApp Scriptは実行出来る
2. クライアントサイドからFetch or XHRで認証のかかっているApp Scriptは実行出来ない

# 詳細

以下実験してみた結果を記載します。

## 調査1

- 普通にFetchしたらどうなる？

出来ました。ただし、Google Apps ScriptにはFetchの機能が限定的にしか使えず設定を工夫する必要がありました。

### 実験したコード

```javascript
// データを取得するApp Script URLをセット
const endpoint = "https://script.google.com/macros/s/*********************/exec";
const xhr = new XMLHttpRequest();

xhr.onload = function () {
	if (xhr.readyState === 4 && xhr.status === 200) {
		console.log(xhr.responseText);
	} else if (xhr.readyState === 4 && xhr.status !== 200) {
		console.log("Error");
	}
}

xhr.open("GET", endpoint, true);
xhr.responseType = "text";
xhr.send(null);
```



※ 前提：呼ばれる側のApp Scriptの権限は全公開

### 限定的だったこと

- FetchでPreflightが発生すると失敗する
- GAS as Web Appは別App Script宛でもCORS Policyに引っかかる

GASの環境下ではPreflightリクエストをする際のOptionに対応していないようでした。この為、以下MDNに記述されているうち単純リクエストにて対応しなければ送付できませんでした。

[MDN CORS](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)

また、GASは以下Developerサイトに記載されている通りiframe内にて実行されています。この為どうしてもクロスドメイン扱いとなってしまいました。

[HTML Service: Restrictions](https://developers.google.com/apps-script/guides/html/restrictions)





## 調査2

- 認証情報を渡したらどうなる？

調査1からクロスドメインになってしまう事がわかったので、全公開していないApp Scriptにもアクセスするべくソースを以下の通りに書き換えました。withCredentialsを付け加えてCookieを持たせることを明示的に示した形です。

```javascript
const endpoint = "https://script.google.com/macros/s/*********************/exec";
const xhr = new XMLHttpRequest();

xhr.onload = function () {
	if (xhr.readyState === 4 && xhr.status === 200) {
		console.log(xhr.responseText);
	} else if (xhr.readyState === 4 && xhr.status !== 200) {
		console.log("Error");
	}
}

xhr.open("GET", endpoint, true);
xhr.withCredentials = true; // 追加
xhr.responseType = "text";
xhr.send(null);
```

これに対するErrorは以下の通りです。

```javascript
Access to XMLHttpRequest at 'https://script.google.com/macros/s/XXXX/exec' from origin 'https://XXXX-0lu-script.googleusercontent.com' 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'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
```



withCredentialsを指定する際にはAPI側のレスポンスは`Access-Control-Allow-Origin:*`であってはならず明示的にOriginを指定せよ、という事のようです。これはクライアント側ではどうしようもないと思いますので次の調査を行いました。



## 調査3

- App ScriptよりResponse Headerの`Access-Control-Allow-Origin`の設定値を変える事は出来るか。

Google Apps Scriptの開発者向けサイトを諸々調べましたがこれの実現方法がありません。私が見落としているだけかもしれませんがこれは全く見つかりませんでした。



## 結果

ページ上部にも記載した通りですが以下の通り結果となりました。元のSpreadSheetの情報は公開したくない、見られたくないデータであるので、今回の調査結果としては達成できそうにありませんでした。

- Fetchする際に認証としてCookieを渡そうとすると失敗する

- 認証さえ不要であれば問題なくアクセス可能



# その他アイディア

上記が出来ないなら以下があるじゃないか、と言われると思いますので記載しておきます。

- App Script APIで関数実行する
- JSONPでやる
- Sheet APIでApp Scriptを経由せずにデータを取得する

今回は上記3点ともセキュリティの問題か用途合わずで使えなかったのですが、こういったやり方もあると思います。



以上、読んで頂きましてありがとうございました。
もしこれなら出来るよ！というやり方があれば是非教えて頂けるとありがたいです。