GAS のウェブアプリでハマった CORS エラーの話

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

問題の概要

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

原因

GoogleAppsScript のコード内でエラーにより異常終了していたことが主な原因でした。 CORS は、付随的な問題であり、主な原因ではありませんでした。 CORS 関連調べまくって特に何も出てこないで時間を無駄にしたので、この記事を書いています。

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

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

このとき、成功側の応答ヘッダーには、「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"}