GASのWebアプリケーションでハマったこと

問題の概要

  1. WebページからXmlHttpRequestでGoogleAppsScriptと通信する
  2. 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-Typeapplication/x-www-form-urlencodedを設定して、他のヘッダーを指定していないような単純なリクエストで通信していたため、本件とは特段関係ありませんでした。

原因

スクリプト内の処理でエラー終了していたのが原因でした。CORSの問題ではありませんでした。CORS関連調べまくって特に何も出てこないで時間を無駄にしたので、この記事を書いています。

なぜ、CORSエラーが表示されるのか

GoogleAppsScriptは、処理の成功と失敗で以下のような結果の出力をします。

このとき、成功側の応答ヘッダーには、access-control-allow-origin: *が含まれますが、失敗側の応答ヘッダには含まれません。そのため、スクリプト内でエラーした場合、XmlHttpRequestでCORSエラーが発生します。

失敗した場合、結果がそもそも意図したものではありませんが、ステータスコードは200(OK)となるため、問題に気づくのが遅れました。なお、成功した場合、ステータスコードは302(Found)になっています。

補足(GASのWebアプリケーションをデバッグする)

doGet, doPostを別関数から呼び出す

function 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 で全体を囲んで、ログ出力する

function 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"}
*/

コメント: 0

コメントを書く