マキモノ
業務自動化無料✅ 公式検証済みv1.0.0 / 更新

Apps Script ゲートウェイ経由の無人ジョブが稀に Unauthorized で落ちる — 本文落ちの判定と有界リトライ

GAS Web App を API ゲートウェイにした Node の夜間ジョブが、トークンも正しいのに稀に Unauthorized / 非JSON で落ちる。受信ログの (no action) で「POST 本文が落ちた一時障害」と 30 秒で判定し、一時障害の署名だけを 2s→4s→8s で有界リトライする分類関数・テスト・本番入口での検証手順まで。

出品者: seisaku-team@orgiast.jp📖 読込 約3,032トークン (約5円)💰 コスパ 13
トークン節約メーター87%節約
ゼロからAIに作らせた場合4.5万トークン
このMDを読ませた場合6,000トークン

3.9万トークンの節約 (API料金換算で約59円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。

この巻物について

Apps Script ゲートウェイ経由の無人ジョブが稀に Unauthorized で落ちる — 本文落ちの判定と有界リトライ」は、業務自動化カテゴリのAI指示書(MDファイル)です。GAS Web App を API ゲートウェイにした Node の夜間ジョブが、トークンも正しいのに稀に Unauthorized / 非JSON で落ちる。受信ログの (no action) で「POST 本文が落ちた一時障害」と 30 秒で判定し、一時障害の署名だけを 2s→4s→8s で有界リトライする分類関数・テスト・本番入口での検証手順まで。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約3.9万トークン(API料金換算で約59円)・87%のトークンを節約できます。

カテゴリ
業務自動化
対応AI
claude-code、cursor、codex-cli
ライセンス
商用利用可 (再販不可)
価格
無料
ゼロから開発時
約4.5万トークン
この巻物使用時
約6,000トークン
節約量
約3.9万トークン (約59円)
更新日
2026-09-13

使い方 (AIに渡す3つの方法)

いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。

⬇ .md をダウンロード
claude "https://makimono-md.vercel.app/api/v1/files/apps-script-unauthorized/raw を読み込んで、この指示書どおりに実装して"
claude-codecursorcodex-cliライセンス: 商用利用可 (再販不可)

中身

Apps Script Web App を API ゲートウェイにしている無人ジョブが、たまに「Unauthorized」で落ちる件の切り分けと直し方

この巻物が解く問題

Node(または任意の HTTP クライアント)から Google Apps Script の Web App(https://script.google.com/macros/s/<デプロイID>/exec)へ JSON を POST し、その中で外部 API(予約システム・会計 SaaS など)を代理呼び出しする「GAS ゲートウェイ」構成で、 夜間ジョブがごく稀に Unauthorized や「JSON でない応答」で落ちる。同じトークン・同じプロセスの数秒前後の呼び出しは通っている。

よくある誤診: 「外部 API のトークンが切れた」「GAS のデプロイが古い」「アクセス権がリセットされた」。 どれも違うことが多い。本文が空のまま GAS に届いているのが真因で、クライアントに再試行が無いだけ。

30 秒で判定する方法(ここを先に見る)

  1. ゲートウェイ側に受信ログを持たせておく(無ければ今すぐ足す。下のコード参照)。認証に失敗したときは 実行元 —/アクション (no action)/Unauthorized のように「payload に何が入っていたか」が分かる形で記録する。
  2. 落ちた時刻の行を見る。
    • (no action) で Unauthorized → リクエスト本文が届いていない。トークンは正しい。
    • アクション名が入っていて Unauthorized → 本当にトークン不一致。資格情報ファイルの mtime と Script Properties を見る。
    • 行が無いのに非 JSON 応答 → Google がスクリプト実行前に HTML エラーページを返した。
  3. 外部 API の 401 なら、ゲートウェイの代理呼び出し部が返す形(例 {ok:false, code:401, path, raw})になるので文言が違う。 単語だけの Unauthorized はゲートウェイ自身の文言。

なぜ本文が落ちるか

fetch(url, { method:'POST', redirect:'follow' }) は Fetch 仕様により 301/302 を追うとき POST を GET に変えて本文を捨てる。 Apps Script の /exec は正常時も 302 で結果ページへ飛ばすが、それは実行後の話。Google 側の負荷・ルーティングで 実行前に別 URL へ 302 を返す瞬間があり、そのとき本文の無い GET が doGet(e) に届く。ゲートウェイは e.parameter = {} からトークンを読めず Unauthorized を返す。もう 1 種は Google が HTML のエラーページを返すもの。 どちらも数秒〜数十秒で回復する一時障害で、観測上の連続回数は 1 回。

直し方: クライアントに「分類 → 有界リトライ」を入れる

分類を純関数に切り出すとネットワーク無しでテストできる。

// gas-client.mjs(Node 18+ / ESM)
export function classifyGasResponse({ status, text }) {
  if (status === 429 || (typeof status === 'number' && status >= 500)) return { kind: 'retry', reason: `HTTP ${status}` };
  let json;
  try { json = JSON.parse(text); } catch { return { kind: 'retry', reason: `非JSON応答 ${String(text).slice(0, 80)}` }; }
  if (json && json.ok === false) {
    if (json.error === 'Unauthorized') return { kind: 'retry', reason: 'Unauthorized(本文が届かなかった可能性)' };
    return { kind: 'fatal', reason: json.error || JSON.stringify(json).slice(0, 200) };
  }
  return { kind: 'ok', json };
}

export async function gas(action, params = {}, opts = {}) {
  const retries = opts.retries ?? Number(process.env.GAS_RETRIES ?? 3);          // 最大 4 回試行
  const base = opts.baseDelayMs ?? Number(process.env.GAS_RETRY_BASE_MS ?? 2000); // 2s → 4s → 8s
  const fetchImpl = opts.fetchImpl ?? ((...a) => globalThis.fetch(...a));
  const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
  let lastReason = '不明';
  for (let attempt = 1; attempt <= retries + 1; attempt++) {
    let outcome;
    try {
      const res = await fetchImpl(process.env.GAS_EXEC_URL, {
        method: 'POST', redirect: 'follow',
        headers: { 'content-type': 'application/json; charset=utf-8' },
        body: JSON.stringify({ token: process.env.GAS_TOKEN, action, params }),
      });
      outcome = classifyGasResponse({ status: res.status, text: await res.text() });
    } catch (e) { outcome = { kind: 'retry', reason: `fetch 失敗: ${e.message}` }; }
    if (outcome.kind === 'ok') return outcome.json;
    if (outcome.kind === 'fatal') throw new Error(`${action}: ${outcome.reason}`);
    lastReason = outcome.reason;
    if (attempt > retries) break;
    const delay = base * 2 ** (attempt - 1);
    console.error(`gas ${action}: ${outcome.reason} → ${attempt}/${retries} 回目の再試行を ${delay}ms 後に実行`);
    await sleep(delay);
  }
  throw new Error(`${action}: ${lastReason}(${retries + 1}回試行して回復せず)`);
}

設計上の要点:

  • 再試行するのは一時障害の署名だけ(HTTP 429/5xx・非 JSON・Unauthorized・fetch 例外)。業務エラー(範囲が大きすぎる等)の ok:false は即失敗にする。何でも再試行すると本当の不具合が 14 秒遅れて同じ文言で出るだけになる。
  • 本当のトークン不一致は同じ文言で失敗するが、末尾の「N回試行して回復せず」で見分けられる。
  • 再試行ごとに stderr へ 1 行出す。夜間ログに残るので「本番で何回吸収したか」が後から数えられる。
  • fetchImpl / sleep を注入可能にし、キュー式のダミー fetch で (a) Unauthorized→OK (b) 非JSON→OK (c) HTTP 500→OK (d) 業務エラーは fetch 1 回で即 throw (e) 全部失敗で遅延が [base, 2base, 4base] (f) fetch 例外→次で成功 をテストする。

ゲートウェイ側(Apps Script)に受信ログを持たせる最小形

// WebApiGateway.gs
var LOG_SHEET = 'リモートAPI実行ログ';
function handleRequest(e, method) {
  var payload = {};
  if (method === 'POST' && e.postData && e.postData.contents) payload = JSON.parse(e.postData.contents);
  else if (e.parameter) payload = e.parameter;
  var expected = PropertiesService.getScriptProperties().getProperty('REMOTE_API_TOKEN');
  if (payload.token !== expected) {
    logRemoteApi('—', payload.action || '(no action)', false, 'Unauthorized');
    return jsonResponse({ ok: false, error: 'Unauthorized' });
  }
  // ... action の dispatch ...
}
function logRemoteApi(who, action, ok, err) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sh = ss.getSheetByName(LOG_SHEET) || ss.insertSheet(LOG_SHEET);
  sh.insertRowAfter(1);
  sh.getRange(2, 1, 1, 5).setValues([[new Date(), who, action, ok ? '✅ OK' : '❌ FAIL', err || '']]);
}

ログの読み方の罠: 日時セルは 2026/09/10 7:01:20 のように時が 1 桁で返るので、正規表現は / ([0-9]{1,2}):/ のように書く (0[678]: と書くと分の 08: に誤マッチする)。readRange に上限(例 2000 セル)があるなら 5 列 × 399 行ずつ読む。

検証は本番と同じ入口で

手元で node job.mjs を叩くのではなく、スケジューラのタスクそのものを起動する(Windows なら schtasks /Run /TN <タスク名>、cron なら同じラッパーを同じユーザーで)。fetch → 固定ブランチの checkout → 実行、という 本番手順が丸ごと走り、LastTaskResult も更新されるので、見張り番(watchdog)の判定と食い違わない。 完了判定は「成果物ファイルが生成された」「ログに NG 行が無い」「再試行行の有無」の 3 つで、成功表示だけで判断しない。

再発防止のチェックリスト

  • ゲートウェイに受信ログがある(失敗時に payload の中身が分かる形)
  • クライアントの再試行は「一時障害の署名だけ」に限定されている
  • 再試行が stderr に残り、夜間ログから回数を数えられる
  • 見張り番の再走が同じ一時障害を踏んでも、1 回の run 内で吸収される(再走 1 回だけに頼らない)
  • 「Unauthorized=認証切れ」と読む前に受信ログのアクション列を見る手順が引き継ぎに書いてある

よくある質問

「Apps Script ゲートウェイ経由の無人ジョブが稀に Unauthorized で落ちる — 本文落ちの判定と有界リトライ」とは何ですか?

GAS Web App を API ゲートウェイにした Node の夜間ジョブが、トークンも正しいのに稀に Unauthorized / 非JSON で落ちる。受信ログの (no action) で「POST 本文が落ちた一時障害」と 30 秒で判定し、一時障害の署名だけを 2s→4s→8s で有界リトライする分類関数・テスト・本番入口での検証手順まで。

どれくらいトークン(費用)を節約できますか?

ゼロから開発すると約4.5万トークンかかりますが、この巻物を使えば約6,000トークンで済みます。差し引き約3.9万トークン(API料金換算で約59円)・87%の節約です。

どうやって使いますか?

無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。

どのAIツールに対応していますか?

claude-code、cursor、codex-cli に対応しています。

商用利用できますか?

ライセンスは「商用利用可 (再販不可)」です。

🤝 自分でAIを動かすのは、まだ不安…という方へ

この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。

AI代行堂を見る →

関連する巻物

この巻物、誰かのトークンも救えます

𝕏 で節約レシートをシェア