GAS のウェブアプリでハマった CORS エラーの話
旧題「GASのWebアプリケーションでハマったこと」
問題の概要
- ウェブページから XmlHttpRequest で GoogleAppsScript と通信する
- GoogleAppsScript 応答を元にウェブページで処理をする
- GoogleAppsScript 応答時に CORS エラーでデータを取得できない ← ここが問題
※記事内は、 XmlHttpRequest ですが fetch でも同様の問題に直面するはずです。
応答
下記のエラーをブラウザの開発者ツールに出力する。(エラーでスクリプトは停止する)
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
を設定して、他のヘッダーを指定していないような単純なリクエストで通信していたため、本件とは特段関係ありませんでした。
原因
GoogleAppsScript のコード内でエラーにより異常終了していたことが主な原因でした。 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: *
」が含まれますが、失敗側の応答ヘッダには含まれません。そのため、 GoogleAppsScript 内で異常終了した場合、 XmlHttpRequest で CORS エラーが発生します。
失敗した場合、結果がそもそも意図したものではありませんが、ステータスコードは 200(OK) となるため、問題に気づくのが遅れました。なお、成功した場合、ステータスコードは 302(Found) になっています。
上記の説明でよく分からなかった人へ
問題の本質は、 CORS エラーではありません。
GoogleAppsScript のコード内でエラーが発生して、 GoogleAppsScript のコードが異常終了している可能性が極めて高いです。 GoogleAppsScript のコードを確認してエラー箇所を修正してください。また、最新コードをデプロイして公開コードを最新コードに更新してください。
それでも問題が解決しないのであれば、下記のサンプルコードを一時的に公開して、ウェブページ側のスクリプトでサンプルのデータを正常に受信できることを確認してください。サンプルのデータを正常に取得できるのであれば、最低でもウェブページ側のスクリプトが問題でないことは確認できるはずです。
サンプル
上記の成功・失敗サンプルのコードです。
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 のウェブアプリをデバッグする)
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"}