offset ページングで外部APIを読むと、取得中に中身が変わって静かに壊れる
ゲートウェイが毎回全文を取り直して offset で切る構成は、取得中に元データが変わると連結が壊れる。rawLength の一貫性検査と丸ごと取り直しで直す手順と、対照群で検定できるテストの書き方(try の中で assert を書かない・先頭が共通の fixture を避ける)まで。
約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 のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/offset-api/raw を読み込んで、この指示書どおりに実装して"
中身
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で頼むだけで、仕事が完成」。
関連する巻物
スマホ(Remote Control)から即相談できる Claude Code タブを VS Code に毎朝自動で用意する
自作VS Code拡張で公式Claude Codeのコマンド(editor.openLast/newConversation/renameSessionTab)を叩き、名前付きタブをN本自動補充。夜間はWM_CLOSE→再起動で毎朝揃える。--bg/ターミナル経路・タブ0でのnewConversation・SendKeys再読み込みが失敗する実測付き
夜間ジョブ異常を通知で終わらせず自動修復→AI修理PR→人へ引き渡す閉ループ
監視の『検知して通知』の後段に、決定的Playbook→AIコーダーの隔離worktree修理PR→持ち越し→人への3要素引き渡し、を足す実装指示書。argvで指示を渡すな等の実測の落とし穴つき
ドキュメント駆動開発プロセス CLAUDE.md — 作るものを固めてから書かせる
「AIが暴走して意図と違うものを作る」を根絶する開発プロセス指示書。UI仕様→機能設計→実装の順をAIに強制し、1ファイルごとに承認ゲートを挟む。受託開発・チーム開発向け。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア