GAS『Cross-Origin Error』の原因と対処法
- 作成日 2025.10.24
- その他
ブラウザから別オリジンのGoogle Apps Script(Webアプリ)や外部サイトへアクセスしたとき、CORS/同一生成元ポリシー、X-Frame-Options、Mixed Contentなどの制限でブロックされる。Apps ScriptはHTTPレスポンスヘッダを自由に設定できないため、UIの配置と通信方式の設計で回避するのが確実。
- 1. 発生条件(症状の切り分け)
- 2. 結論先取り:設計の方針
- 3. 対処1:HtmlService + google.script.run で同一オリジン化
- 4. 対処2:プリフライト回避(Simple Requestに寄せる)
- 5. 対処3:iframe埋め込み+postMessage(画面は外部、処理はGAS)
- 6. 対処4:プロキシを用意(Cloud Run/Functions等でCORSヘッダ付与)
- 7. 対処5:GASから外部APIへはサーバ側(UrlFetchApp)で実行
- 8. 対処6:Mixed Contentと資格情報の落とし穴
- 9. 対処7:GASでCORSヘッダを付与できない前提で設計
- 10. デバッグ:実際に送って結果を可視化
- 11. NG→OK早見表
- 12. 最小テンプレ(シンプルPOSTでプリフライト回避)
- 13. チェックリスト(導入前に)
発生条件(症状の切り分け)
・外部サイトのフロントエンド(example.com)から、GASのWebアプリURL(script.google.com)にfetch/XHRでアクセス
・オリジンが異なるためプリフライト(OPTIONS)が発生し、CORSヘッダ不足で失敗
・GASのHTMLを他ドメインでiframe埋め込み → X-Frame-Options違反やpostMessage未実装で通信不可
・HTTPSページからHTTPにアクセス → Mixed Contentでブロック
・Cookie/認証つきのクロスオリジン → ブラウザの制約で遮断
結論先取り:設計の方針
・「GASのUIはGASで出し、サーバ呼び出しはgoogle.script.runで完結」=同一オリジン化
・外部サイトからGASを直叩きしない。必要なら「プロキシ(Cloud Run/Functions等)」を用意
・どうしてもブラウザ直叩きなら「プリフライトが要らないシンプルリクエスト」に寄せる
・埋め込みはHtmlServiceのXFrameOptions許可+postMessageで橋渡し
対処1:HtmlService + google.script.run で同一オリジン化
function doGet() {
return HtmlService
.createHtmlOutputFromFile('index')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.SAMEORIGIN); // 基本は同一オリジン
}
// index.html(GASが配信するUI内でサーバ関数を呼ぶ)
<script>
function sendData() {
google.script.run
.withSuccessHandler(resp => console.log(resp))
.serverSide({foo: 'bar'});
}
</script>・GAS配信のHTML内ならCORSは発生しない。最も安定する構成。
対処2:プリフライト回避(Simple Requestに寄せる)
・HTTPメソッドはGET/POSTのみ。
・ヘッダはカスタムを付けない(Authorization等を使わない)。
・Content-Typeは「application/x-www-form-urlencoded」「text/plain」「multipart/form-data」のいずれか。
・BodyはURLエンコードかFormDataで渡し、GAS側ではe.parameterで受け取る。
/** GAS側:form-urlencodedを受け取る */
function doPost(e) {
var p = e.parameter || {};
var name = p.name || '';
return ContentService.createTextOutput('ok:' + name)
.setMimeType(ContentService.MimeType.TEXT);
}
/** フロント側:プリフライトを起こさないPOST */
fetch('https://script.google.com/macros/s/DEPLOY_ID/exec', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ name: 'Alice' }) // 文字列化必須
}).then(r => r.text()).then(console.log);・カスタムヘッダやapplication/jsonを外すと、多くのケースでOPTIONSが発生しない。
対処3:iframe埋め込み+postMessage(画面は外部、処理はGAS)
・GAS側ページをiframeで読み込み、親⇄子をpostMessageで連携。
・GASページはXFrameOptionsを許可(ALLOWALL / SAMEORIGIN)。
// GAS側
function doGet() {
return HtmlService.createHtmlOutputFromFile('embed')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// 親ページ(外部サイト)
<iframe id="gas" src="https://script.google.com/macros/s/DEPLOY_ID/exec"></iframe>
<script>
const frame = document.getElementById('gas').contentWindow;
window.addEventListener('message', ev => {
if (ev.origin.endsWith('script.google.com')) {
console.log('from GAS:', ev.data);
}
});
// 送信
frame.postMessage({cmd:'ping'}, 'https://script.google.com');
</script>・XHRは使わず、表示+メッセージ通信で連携するアプローチ。
対処4:プロキシを用意(Cloud Run/Functions等でCORSヘッダ付与)
・外部サイト →(ブラウザ)→ プロキシ(自社ドメイン・CORS許可)→(サーバ間)→ GAS/他API。
・プロキシはAccess-Control-Allow-Origin/Methods/Headers/Credentials等を正しく返す。
・Apps Script側はUrlFetchAppでプロキシからサーバ間で呼ぶ設計に切替えるのも手。
対処5:GASから外部APIへはサーバ側(UrlFetchApp)で実行
・ブラウザではなくGASサーバ側で外部APIを呼び出せば、CORSは関係ない(サーバ間通信)。
・クライアントはgoogle.script.runでGASを叩き、結果のみを受け取る。
function callExternal() {
var res = UrlFetchApp.fetch('https://api.example.com/data');
return res.getContentText();
}
function doGet() {
return HtmlService.createHtmlOutput('<script>google.script.run.withSuccessHandler(alert).callExternal()</script>');
}対処6:Mixed Contentと資格情報の落とし穴
・HTTPSページからHTTPにアクセスするとMixed Contentでブロック。双方HTTPSに統一。
・cookie/credentialを伴うクロスオリジンfetchは厳しい。ブラウザ制約とCORSが揃って必要。
対処7:GASでCORSヘッダを付与できない前提で設計
・ContentService/HtmlServiceから任意レスポンスヘッダを設定するAPIはない。
・「GASにCORSヘッダを付ければ解決」は基本不可能。前述の同一オリジン化/プロキシ/プリフライト回避で対処。
デバッグ:実際に送って結果を可視化
/** 受信ログを確認(何が届いているか) */
function doPost(e) {
console.log(JSON.stringify({
type: e && e.postData && e.postData.type,
length: e && e.postData && e.postData.length,
params: e && e.parameter
}));
return ContentService.createTextOutput('ok').setMimeType(ContentService.MimeType.TEXT);
}・ブラウザのネットワークタブでRequest/Response/Consoleのエラー種別(CORS/X-Frame/Mixed)を確認。
NG→OK早見表
・NG:外部サイトからGASにapplication/json+カスタムヘッダでPOST → OK:x-www-form-urlencodedでシンプルPOST
・NG:外部SPAがGASをXHR直叩き → OK:Cloud Run等のプロキシ経由でCORS許可
・NG:GASのページを他ドメインにiframe埋め込み、内部でXHR → OK:XFrameOptions許可+postMessageで連携
・NG:フロントから直接外部API → OK:GAS(サーバ)経由でUrlFetchAppし、結果だけ返す
最小テンプレ(シンプルPOSTでプリフライト回避)
/** GAS */
function doPost(e) {
var p = e.parameter || {};
return ContentService.createTextOutput('hello ' + (p.name || ''))
.setMimeType(ContentService.MimeType.TEXT);
}
/** フロント(別ドメイン) */
const url = 'https://script.google.com/macros/s/DEPLOY_ID/exec';
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ name: 'Taro' })
}).then(r => r.text()).then(console.log).catch(console.error);チェックリスト(導入前に)
・UIはGAS側で提供できるか?できるならgoogle.script.runに寄せる
・外部サイトから直叩きが必要か?必要ならプリフライト回避の条件を満たしているか
・iframe埋め込みならXFrameOptionsとpostMessageの設計があるか
・プロキシ(Cloud Run/Functions/Workers)の用意でCORSを自前制御できるか
・HTTPS/同一ドメインで統一できているか(Mixed Content回避)
・Cookie/認証を絡めたクロスオリジン要求が本当に必要か(設計変更の余地を検討)
-
前の記事
GAS『Exceeded Maximum Email Recipients per Day』の原因と対処法 2025.10.23
-
次の記事
GAS『Uncaught Exception』の原因と対処法 2025.10.27
コメントを書く