GASのWebアプリケーションでハマったこと
問題の概要
- WebページからXmlHttpRequestでGoogleAppsScriptと通信する
- GoogleAppsScript応答を元にWebページで処理をする
- GoogleAppsScript応答時にCORSエラーでデータを取得できない ← ここが問題
応答
下記のエラーをブラウザの開発者ツールに出力する。(エラーでスクリプトは停止する)
Chrome
Access to XMLHttpRequest at 'https://script.google.com/macros/s/AKfycbw1uH-oQ8FO4RmdXzRdykkFUdcBPMce4EJ1xcRTd1qG8T-S5kE/exec' from origin 'https://www.bugbugnow.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Firefox
クロスオリジン要求をブロックしました: 同一生成元ポリシーにより、https://script.google.com/macros/s/AKfycbw1uH-oQ8FO4RmdXzRdykkFUdcBPMce4EJ1xcRTd1qG8T-S5kE/exec にあるリモートリソースの読み込みは拒否されます (理由: CORS ヘッダー ‘Access-Control-Allow-Origin’ が足りない)。
補足(JSONPの場合)
JSONPとしても下記の警告を表示する。
Chrome
Cross-Origin Read Blocking (CORB) blocked cross-origin response https://script.google.com/macros/s/AKfycbw1uH-oQ8FO4RmdXzRdykkFUdcBPMce4EJ1xcRTd1qG8T-S5kE/exec?callback=test&error=err with MIME type text/html. See https://www.chromestatus.com/feature/5629709824032768 for more details.
Firefox
MIME タイプ (“text/html”) の不一致により “https://script.google.com/macros/s/AKfycbw1uH-oQ8FO4RmdXzRdykkFUdcBPMce4EJ1xcRTd1qG8T-S5kE/exec?callback=test&error=err” からのリソースがブロックされました (X-Content-Type-Options: nosniff)。
調べて特に関係なかったこと
プリフライトリクエスト(OPTIONSメソッド)
ブラウザは、単純なリクエストでない場合、GETなどのメソッドの送信前にOPTIONSメソッドを自動的に送信します。GoogleAppsScriptは、OPTIONSメソッドに対応していない?ため、CORSエラーとなる。
GETやPOSTメソッドでContent-Type
にapplication/x-www-form-urlencoded
を設定して、他のヘッダーを指定していないような単純なリクエストで通信していたため、本件とは特段関係ありませんでした。
原因
スクリプト内の処理でエラー終了していたことが主な原因でした。CORSは、付随的な問題であり、主な原因ではありませんでした。CORS関連調べまくって特に何も出てこないで時間を無駄にしたので、この記事を書いています。
なぜ、CORSエラーが表示されるのか
GoogleAppsScriptは、処理の成功と失敗で以下のような結果の出力をします。
- 成功の場合
- 要求URL: https://script.google.com/macros/s/AKfycbw1uH-oQ8FO4RmdXzRdykkFUdcBPMce4EJ1xcRTd1qG8T-S5kE/exec?a=abc
- 出力URL: https://script.googleusercontent.com/macros/echo?user_content_key=9vzoBbQX9BetXsgkJfJ-uViUvMS5yhiHHyFw970dv9OREWMWE3XC-m5JHuBxIEKIsrrttITZsZe-JkIsmDf_4EekzUGndTFjm5_BxDlH2jW0nuo2oDemN9CCS2h10ox_1xSncGQajx_ryfhECjZEnMNqeOndzXRsTzjiwx9wBqiidjItq8OWJEsK0R3u86c6UjhZQTuMhz7zicrqSB8Uz_pNNcEnoskCmjhihpkOzwI&lib=MTlhb09PVDl_eh2Dk0TGcNwfbNS0rMw0b
- 結果: {"v":1,"a":"abc"}
- 失敗の場合
- 要求URL: https://script.google.com/macros/s/AKfycbw1uH-oQ8FO4RmdXzRdykkFUdcBPMce4EJ1xcRTd1qG8T-S5kE/exec?a=abc&error=xyz
- 出力URL: https://script.google.com/macros/s/AKfycbw1uH-oQ8FO4RmdXzRdykkFUdcBPMce4EJ1xcRTd1qG8T-S5kE/exec?a=abc&error=xyz
- 結果: Error: xyz(行 14、ファイル「コード」、プロジェクト「Webアプリケーションテスト」)
このとき、成功側の応答ヘッダーには、access-control-allow-origin: *
が含まれますが、失敗側の応答ヘッダには含まれません。そのため、スクリプト内でエラーした場合、XmlHttpRequestでCORSエラーが発生します。
失敗した場合、結果がそもそも意図したものではありませんが、ステータスコードは200(OK)となるため、問題に気づくのが遅れました。なお、成功した場合、ステータスコードは302(Found)になっています。
サンプル
上記の成功・失敗サンプルのコードです。
function doGet(e) {
return doAction(e);
}
function doPost(e) {
return doAction(e);
}
// Web呼び出しへの応答
function doAction(e, debug) {
// 応答データ作成
var json = {};
if (e.parameter.error) {
throw new Error(e.parameter.error);
} else {
json = {v:1};
var keys = Object.keys(e.parameter);
for (var i=0; i<keys.length; i++) {
json[keys[i]] = e.parameter[keys[i]];
}
}
// 戻り値作成
var out = null;
if (e.parameter.callback) {
var text = e.parameter.callback + '(' + (debug ? JSON.stringify(json, null, 2) : JSON.stringify(json)) + ')';
out = ContentService.createTextOutput(text);
out.setMimeType(ContentService.MimeType.JAVASCRIPT);
} else {
out = ContentService.createTextOutput((debug ? JSON.stringify(json, null, 2) : JSON.stringify(json)));
out.setMimeType(ContentService.MimeType.JSON);
}
return out;
}
// アクション呼び出し用テスト関数
function doTest() {
var e = {
parameter: {
a: 'abc',
//error: 'xyz' // コメントアウトで成功・失敗を分岐
}
};
var out = doAction(e, true);
Logger.log('MimeType: '+out.getMimeType());
Logger.log('Content: \n'+out.getContent());
}
補足(GASのWebアプリケーションをデバッグする)
doGet, doPostを別関数から呼び出す
doGetDebug.jsfunction doGet(e) {
// 応答データ作成
var json = {};
json.data = e.parameter.a;
// 戻り値作成
var out = ContentService.createTextOutput(JSON.stringify(json));
out.setMimeType(ContentService.MimeType.TEXT);
return out;
}
function doTest() {
var e = {
parameter: {
a: 'abc'
}
};
var out = doGet(e);
Logger.log('MimeType: '+out.getMimeType());
Logger.log('Content: \n'+out.getContent());
}
/*
実行結果例
[19-10-04 22:37:20:653 JST] MimeType: TEXT
[19-10-04 22:37:20:654 JST] Content:
{"data":"abc"}
*/
try catch で全体を囲んで、ログ出力する
tryCatchDebug.jsfunction doGet(e) {
var json = {};
var logs = [];
try {
// メインの処理
// ...
logs.push('Hello World');
// ...
throw new Error('xyz');
} catch (e) {
logs.push(e);
json.logs = logs.join('\n');
}
// 戻り値作成
var out = ContentService.createTextOutput(JSON.stringify(json));
out.setMimeType(ContentService.MimeType.TEXT);
return out;
}
/*
実行結果例
{"logs":"Hello World\nError: xyz"}
*/