GAS『Server Error: Internal Error』の原因と対処法

GAS『Server Error: Internal Error』の原因と対処法

Google Apps Scriptや内部Googleサービスが一時的に失敗したときに返る汎用エラー。ネットワークの瞬断、バックエンドの一時障害、レート/同時実行の突発上限、巨大ペイロード、長時間ロック、タイムアウト連鎖などが誘因。恒久的なバグと切り分け、指数バックオフ・チャンク処理・同時実行制御・冪等化で落ちにくい実装へ寄せる。

エラーの概要と発生条件(まず押さえる)

・「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操作はチャンク化・スロットリングしているか

・処理は冪等キーで再試行安全になっているか

・長時間処理はカーソル保存+再開で分割しているか

・ログ/アラートの経路を整備し、再発時に即座に原因が追えるか