GAS『Server Error: Internal Error』の原因と対処法
- 作成日 2025.10.30
- その他
Google Apps Scriptや内部Googleサービスが一時的に失敗したときに返る汎用エラー。ネットワークの瞬断、バックエンドの一時障害、レート/同時実行の突発上限、巨大ペイロード、長時間ロック、タイムアウト連鎖などが誘因。恒久的なバグと切り分け、指数バックオフ・チャンク処理・同時実行制御・冪等化で落ちにくい実装へ寄せる。
- 1. エラーの概要と発生条件(まず押さえる)
- 2. 最短復旧フロー(再実行可能性の判定→切り分け→暫定復旧)
- 3. 指数バックオフ+ジッタ(再試行の標準パターン)
- 4. 巨大書き込みはチャンクに分割(setValues/コピー/Drive操作)
- 5. UrlFetchはfetchAll+タイムアウト短縮で粘る
- 6. 同時実行の競合を回避(LockServiceによる排他制御)
- 7. 冪等化(再試行しても二重にならない設計)
- 8. 入口を安全化(doGet/doPost/onEditのガードと整形応答)
- 9. 長時間実行・メモリ超過の予防(早期終了と再開)
- 10. 原因特定のための最小再現テンプレ(I/Oを一点に絞る)
- 11. 外部APIのレスポンス異常に備える(検証+フォールバック)
- 12. 監視とアラート(失敗を早く知る)
- 13. NG→OK早見表(クイック修正)
- 14. チェックリスト(上から順に潰す)
エラーの概要と発生条件(まず押さえる)
・「Server error」や「Internal error」は原因非公開のサーバ側失敗を示す(リトライで成功することが多い)
・UrlFetchApp/Sheets/Drive/Calendar/GmailなどI/O系で出やすい
・大量データのsetValues/スクリプト同時実行/ネットワーク変動/一時的なサービス劣化が誘因
・TypeErrorやSyntax Errorなど“恒久バグ”とは切り分けが必要
最短復旧フロー(再実行可能性の判定→切り分け→暫定復旧)
1) 直前の変更がないのに突発的に出たか(Yesなら再試行で復旧見込みが高い)
2) 実行ログでどのI/Oで落ちたか特定(fetch、setValues、Drive操作など)
3) 一時復旧:指数バックオフ+ジッタで自動再試行、サイズや同時実行を下げる
4) 恒久対策:チャンク化、ロック制御、冪等化、タイムアウト短縮、監視
指数バックオフ+ジッタ(再試行の標準パターン)
function withRetry(fn, tries = 5, baseMs = 400, maxMs = 8000) {
let wait = baseMs;
for (let i = 0; i < tries; i++) {
try {
return fn();
} catch (e) {
const msg = String(e && e.message || '');
const transient = /internal|server error|backend|unavailable|quota|rate|timeout/i.test(msg);
if (!transient || i === tries - 1) throw e;
Utilities.sleep(wait + Math.floor(Math.random() * 250)); // ジッタ
wait = Math.min(wait * 2, maxMs);
}
}
}巨大書き込みはチャンクに分割(setValues/コピー/Drive操作)
function setValuesChunked(sh, r, c, values2d) {
if (!Array.isArray(values2d) || !Array.isArray(values2d[0])) throw new Error('2次元配列が必要');
const COLS = values2d[0].length;
const MAX_CELLS = 50000; // 1回の安全上限目安
let i = 0;
while (i < values2d.length) {
const rows = Math.min(values2d.length - i, Math.floor(MAX_CELLS / COLS));
const block = values2d.slice(i, i + rows);
withRetry(() => sh.getRange(r + i, c, block.length, COLS).setValues(block));
i += rows;
Utilities.sleep(120); // 軽いスロットリング
}
}UrlFetchはfetchAll+タイムアウト短縮で粘る
function fetchParallel(urls) {
const reqs = urls.map(u => ({ url: u, muteHttpExceptions: true, followRedirects: true, timeout: 15000 }));
return withRetry(() => UrlFetchApp.fetchAll(reqs))
.map(r => ({ code: r.getResponseCode(), body: r.getContentText() }));
}同時実行の競合を回避(LockServiceによる排他制御)
function withSpreadsheetLock(work, waitMs = 20000) {
const lock = LockService.getDocumentLock();
if (!lock.tryLock(waitMs)) throw new Error('ロック取得に失敗');
try { return work(); }
finally { lock.releaseLock(); }
}
// 例:ログ書き込みを排他で守る
function appendLogSafe(ssId, sheetName, row) {
return withSpreadsheetLock(() => {
const sh = SpreadsheetApp.openById(ssId).getSheetByName(sheetName);
sh.appendRow(row);
});
}冪等化(再試行しても二重にならない設計)
function idempotentWrite(ssId, sheetName, key, row) {
const sh = SpreadsheetApp.openById(ssId).getSheetByName(sheetName);
const last = sh.getLastRow();
const vals = last ? sh.getRange(1,1,last,1).getValues().flat() : [];
if (vals.includes(key)) return 'skip';
withRetry(() => sh.appendRow([key].concat(row)));
return 'wrote';
}入口を安全化(doGet/doPost/onEditのガードと整形応答)
function safeRun(entry, ctx = {}) {
try { return entry(); }
catch (e) {
console.error('internal', { msg: e.message, stack: e.stack, ctx });
// 必要なら通知やフォールバック
throw e;
}
}
function doPost(e) {
return safeRun(() => {
const body = e?.postData?.contents || '';
const result = process(body); // 内部でwithRetry/ロックを活用
return ContentService.createTextOutput(JSON.stringify({ ok: true, result }))
.setMimeType(ContentService.MimeType.JSON);
}, { entry: 'doPost', len: e?.postData?.length });
}長時間実行・メモリ超過の予防(早期終了と再開)
function shouldStop(startMs, limitMs = 5 * 60 * 1000) {
return Date.now() - startMs > limitMs;
}
function processLargeList(items) {
const p = PropertiesService.getScriptProperties();
let idx = Number(p.getProperty('cursor') || 0);
const start = Date.now();
for (; idx < items.length; idx++) {
withRetry(() => heavyWork(items[idx]));
if (shouldStop(start)) break; // 実行時間上限前に自発的に抜ける
}
p.setProperty('cursor', String(idx));
if (idx < items.length) ScriptApp.newTrigger('resume').timeBased().after(60 * 1000).create();
}
function resume() {
const items = loadItems();
processLargeList(items);
}原因特定のための最小再現テンプレ(I/Oを一点に絞る)
function reproduce() {
try {
const id = 'YOUR_SHEET_ID';
const sh = SpreadsheetApp.openById(id).getSheetByName('Data');
withRetry(() => sh.getRange('A1').setValue(new Date().toISOString()));
return 'ok';
} catch (e) {
console.error('repro failed', { msg: e.message, stack: e.stack });
throw e;
}
}外部APIのレスポンス異常に備える(検証+フォールバック)
function getJson(url) {
const res = withRetry(() => UrlFetchApp.fetch(url, { muteHttpExceptions: true, timeout: 15000 }));
const code = res.getResponseCode();
if (code >= 500) throw new Error('Server error ' + code);
if (code >= 400) throw new Error('Client error ' + code);
const text = res.getContentText() || '';
try { return JSON.parse(text); }
catch (_) { return { raw: text }; } // 最低限のフォールバック
}監視とアラート(失敗を早く知る)
function logError(e, ctx) {
const sh = SpreadsheetApp.openById('LOG_SHEET_ID').getSheetByName('Errors');
sh.appendRow([new Date(), e.name || '', e.message || '', (e.stack || '').slice(0,500), JSON.stringify(ctx).slice(0,500)]);
}NG→OK早見表(クイック修正)
・NG:大量setValuesを一括実行 → OK:5万セル以下にチャンク+スロットリング
・NG:直列UrlFetchで全件 → OK:fetchAll+短タイムアウト+バックオフ
・NG:同時起動でappendRow競合 → OK:LockServiceで排他、書き込みはまとめる
・NG:再試行で二重登録 → OK:冪等キーで重複検知・スキップ
・NG:実行時間上限で中断→再実行で二重処理 → OK:カーソル保存+再開トリガー
・NG:例外が上位まで素通り → OK:safeRunで捕捉・整形ログ・通知
チェックリスト(上から順に潰す)
・直近のコード変更がなく発生?→まず自動再試行を導入
・失敗I/Oを特定し、サイズと同時実行を下げたか(チャンク/ロック)
・UrlFetchのタイムアウトと並列化・バックオフは設定済みか
・setValuesやDrive操作はチャンク化・スロットリングしているか
・処理は冪等キーで再試行安全になっているか
・長時間処理はカーソル保存+再開で分割しているか
・ログ/アラートの経路を整備し、再発時に即座に原因が追えるか
-
前の記事
GAS『DriveApp cannot be used in this context』の原因と対処法 2025.10.28
-
次の記事
GAS『Unknown Error Occurred』の原因と対処法 2025.10.31
コメントを書く