マキモノ
開発プロセス無料✅ 公式検証済みv1.0.0 / 更新

offset ページングで外部APIを読むと、取得中に中身が変わって静かに壊れる

ゲートウェイが毎回全文を取り直して offset で切る構成は、取得中に元データが変わると連結が壊れる。rawLength の一貫性検査と丸ごと取り直しで直す手順と、対照群で検定できるテストの書き方(try の中で assert を書かない・先頭が共通の fixture を避ける)まで。

出品者: seisaku-team@orgiast.jp📖 読込 約2,576トークン (約4円)💰 コスパ 8
トークン節約メーター78%節約
ゼロからAIに作らせた場合2.5万トークン
このMDを読ませた場合5,500トークン

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

この巻物について

offset ページングで外部APIを読むと、取得中に中身が変わって静かに壊れる」は、開発プロセスカテゴリのAI指示書(MDファイル)です。ゲートウェイが毎回全文を取り直して offset で切る構成は、取得中に元データが変わると連結が壊れる。rawLength の一貫性検査と丸ごと取り直しで直す手順と、対照群で検定できるテストの書き方(try の中で assert を書かない・先頭が共通の fixture を避ける)まで。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約2万トークン(API料金換算で約29円)・78%のトークンを節約できます。

カテゴリ
開発プロセス
対応AI
claude-code、cursor、codex-cli
ライセンス
商用利用可 (再販不可)
価格
無料
ゼロから開発時
約2.5万トークン
この巻物使用時
約5,500トークン
節約量
約2万トークン (約29円)
更新日
2026-09-21

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

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

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

中身

offset ページングで外部APIを読むと、取得中に中身が変わって静かに壊れる

誰向けか

サーバーレスのゲートウェイ(GAS Web App / Cloud Functions / Workers など)を挟んで 外部APIの長い応答を読んでいる人。とくに「応答が長すぎて切れるので offset で継ぎ足している」人。

症状

  • 普段は動くのに、たまに JSON.parse が落ちる。再実行すると直る。
  • 落ちた時のエラーは「JSON にならない(<長い数字>字)」のように、応答そのものは取れているように見える
  • 夜間バッチなど無人の時間帯に偏って起きる(人が見ていない間にデータが動くから)。
  • 最悪の場合は例外すら出ず、中身だけ別物の妥当な JSON が返る。

原因

ゲートウェイ側がこう書かれていると起きる。

function proxyGet(path, offset) {
  var res  = UrlFetchApp.fetch(API_BASE + path, { headers: { token: TOKEN } });
  var full = res.getContentText();        // ← 呼ばれるたびに全文を取り直している
  var LIMIT = 45000;
  var off   = Number(offset) || 0;
  var chunk = full.substring(off, off + LIMIT);
  return {
    rawLength : full.length,
    offset    : off,
    nextOffset: (off + chunk.length < full.length) ? (off + chunk.length) : null,
    raw       : chunk
  };
}

スナップショットを保持していない。 offset ごとに毎回、外部APIを叩き直して切っている。

呼び出し側が素直に継ぎ足すと、こうなる。

// これが壊れる書き方
let raw = '', offset = 0;
for (let i = 0; i < 40; i++) {
  const r = await proxy('get', { path, offset });
  raw += r.raw || '';
  if (r.nextOffset == null) break;
  offset = r.nextOffset;
}
return JSON.parse(raw);   // ← ときどき落ちる

チャンク1本が数万字だと1呼び出しに十数秒かかる。3チャンクで約1分。 その1分の間に元データが1件増える/減る/変わると、本文の長さが変わる。 すると chunk0 は古い本文から、chunk1 以降は新しい本文から、それぞれ固定 offset で切られる。 連結した文字列はどちらの本文でもない。

直し方(呼び出し側だけで完結する。ゲートウェイの再デプロイは不要)

ゲートウェイが rawLength(=その時の全文の長さ)を返しているなら、それが鍵になる。 返していないなら、まずそれだけ足す。

export async function getJsonPaged(pathAndQuery, deps = {}) {
  const call  = deps.call  ?? proxy;
  const sleep = deps.sleep ?? (ms => new Promise(r => setTimeout(r, ms)));
  const history = [];
  let lastRaw = '';

  for (let attempt = 1; attempt <= 3; attempt++) {
    let raw = '', offset = 0, expected = null, consistent = true;

    for (let i = 0; i < 40; i++) {
      const r = await call('get', { path: pathAndQuery, offset });

      // ① 一貫性検査: 途中で全文の長さが変わったら、その試行は捨てる
      if (r.rawLength != null) {
        if (expected === null) expected = r.rawLength;
        else if (r.rawLength !== expected) { consistent = false; break; }
      }

      raw += r.raw || '';
      if (r.nextOffset == null) break;
      offset = r.nextOffset;
    }

    history.push(expected);
    lastRaw = raw;

    // ② 連結長の検算
    const lengthOk = (expected === null) || (raw.length === expected);

    if (consistent && lengthOk) {
      try { return JSON.parse(raw); } catch { /* ③ へ */ }
    }

    // ③ 丸ごと取り直す(部分的な継ぎ足し修復はしない)
    if (attempt < 3) await sleep(attempt === 1 ? 1000 : 2000);
  }

  throw new Error(
    `ページング取得に失敗(${lastRaw.length}字・3回試行・長さ履歴 [${history.join(', ')}])` +
    lastRaw.slice(0, 200)
  );
}

設計上の要点

  • 部分的に直そうとしない。 本文の途中がどう変わったかは分からないので、offset 0 から丸ごと取り直す。
  • rawLength が無い応答では検査をスキップして従来どおり動かす。 古いゲートウェイと共存できる。
  • 例外メッセージの前半は変えない。 ログや監視が grep している可能性がある。診断情報は後ろに足す。
  • deps で依存を注入できるようにする。 これが無いとテストが書けない(後述)。
  • 既存の呼び出し側は引数を増やさずにそのまま動くようにする。

テストの落とし穴(ここで2回失敗した)

落とし穴1: モックが「フラグ」だけ変えて、本文を変えていない

rawLength の値だけずらし、返す文字列は同じものから切っていると、 ガードを全部外しても テストが通る。腐敗が再現できていないから。

必ず 2つの別の本文を用意し、チャンクの途中で切り替える。

落とし穴2: 新旧の本文の「先頭が共通」だと腐敗しない

件数を1件足しただけの本文は、前半が元の本文とバイト単位で一致する。 最初のチャンクが共通部分に収まると、連結結果が偶然そのまま正しい JSON になる。

先頭から違う本文にする(配列の先頭要素を変える、など)。

落とし穴3: try の中で assert を書く

// これは何が起きても通る
try {
  const result = await naive(...);
  assert.notDeepEqual(result, expected);   // これが投げる AssertionError も
} catch {
  assert.ok(true, '期待どおり例外が出た');    // ここが飲み込む
}

判定は catch の外でやる。

let threw = false, result;
try { result = await naive(...); } catch { threw = true; }
assert.ok(threw || !isDeepStrictEqual(result, expected), '...');

必ずやること: 対照群

テストを書いたら、ガードをコメントアウトして落ちることを確認する。 落ちなければ、それはテストではなく飾り。

このとき潰し方も検めるconsistent = false; の行だけ消しても直後の break; が残っていれば ガードは半分生きている。無効化したつもりの行が本当に効いていたか、必ず現物を見る。

残る限界(正直に書いておく)

新旧の本文が 同じ長さ で中身だけ変わる場合、上の2つの検査は素通りする。 混ざった結果が構文として妥当な JSON になり、例外も出さずに誤った値が返る。

  • 実害が出るか: レコードの増減は長さが変わるので捕まる。長さ中立の変化は タイムスタンプの更新程度なので、多くの用途では影響しない。
  • 塞ぐなら: チャンク境界を数十字だけ重ねて取得し、重なり部分が一致するか突き合わせる。 追加のAPI呼び出しは発生しない(各チャンクを少し多めに取るだけ)。

直さないと決めたなら、テストを消さずに { todo: '理由' } を付けて残す。 多くのテストランナーは todo の失敗をスイートの失敗に数えないので、 証拠を残したまま先へ進める。消すと次の担当者が同じ穴を踏む。

効く範囲を先に数える

このバグは「1つのツールの不具合」に見えるが、ページング関数を使っている全箇所に効いている。 直す前に grep で呼び出し側を数え、その一覧を報告に入れること。 1箇所で再現したから1箇所の問題、とは限らない。

再発を防ぐ運用

  • 「いま実行したら動いた」を根拠に直ったと判断しない。競合状態は普段は動く。
  • 無人ジョブが失敗した時の子プロセスの stderr を捨てない。 stdio: 'inherit' にすると error.stderr が空になり、失敗理由が汎用文言に潰れる。 ['ignore','pipe','pipe'] で受けて、追記ログへ落とす(上書き形式の状態ファイルは次の実行で消える)。
  • 再試行の上限を「成功回数」で数えない。「走ったが成果物が出ない」試行が数えられないと、 上限が効かずに無人で回り続ける。

よくある質問

「offset ページングで外部APIを読むと、取得中に中身が変わって静かに壊れる」とは何ですか?

ゲートウェイが毎回全文を取り直して offset で切る構成は、取得中に元データが変わると連結が壊れる。rawLength の一貫性検査と丸ごと取り直しで直す手順と、対照群で検定できるテストの書き方(try の中で assert を書かない・先頭が共通の fixture を避ける)まで。

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

ゼロから開発すると約2.5万トークンかかりますが、この巻物を使えば約5,500トークンで済みます。差し引き約2万トークン(API料金換算で約29円)・78%の節約です。

どうやって使いますか?

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

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

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

商用利用できますか?

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

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

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

AI代行堂を見る →

関連する巻物

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

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