CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/API & SDK
API & SDK/2026-05-09上級

Claude API の 400/422/529 を“呼ぶ前”に消す5層プリフライト設計 — 月数百件の失敗呼び出しをゼロにした実装

Claude API の 400/422/529 エラーを“呼ぶ前”に止める5層プリフライト設計を、TypeScript の完全実装と運用1ヶ月分の数値でまとめました。失敗呼び出しのコスト・リトライ・通知ノイズを同時に削減できます。

claude-api81validation2preflight3production87error-handling10

2026年の冬、自分が運営する個人 SaaS の Sentry 通知が、毎朝 30〜80 件の invalid_request_error で埋まっていました。中身を一つずつ追いかけてみると、messagesrole 順序が崩れているもの、tools[].input_schematype フィールドが抜けているもの、max_tokens がモデル上限を超えているもの、と、原因はバラバラでした。共通しているのは「Anthropic のサーバーまでリクエストが飛んで、そこで初めて 400 が返ってきている」ことだけです。

リトライしても通らない、料金は微量とはいえ毎回発生する、Sentry のノイズで本当に見るべきインシデントが埋もれる ―― この三重苦に一週間ほど悩んだあと、私はようやく気づきました。サーバーに問い合わせる前にクライアント側で同じ判定をすれば、この失敗は呼ぶ必要すらなかったのです。プリフライト(preflight)と呼ばれる、HTTP の世界では古典的な発想を Claude API 専用に組み直してみたのが本稿で紹介する設計です。実装後の1ヶ月で、invalid_request_error は 412 件 → 0 件、overloaded_error の通知も体感の半分以下に落ち着きました。同じ景色に疲れている方の参考になれば嬉しいです。

「呼ばずに済むものは呼ばない」というプリフライトの設計思想

リトライやサーキットブレーカーは「壊れた呼び出しをどう受け止めるか」の話です。プリフライトはそれより1つ上流、「壊れる呼び出しを発生させない」を目的にします。視点を1段あげるだけで、3つのコストが同時に減ります。

  • API 料金: 400 系でも usage は 0 ですが、リトライをかけている場合は 200 が返るまで何度も叩いて課金されます。プリフライトは課金が走り始める前にエラーを返すため、無駄な往復料金がそもそも発生しません
  • インフラ料金: Cloudflare Workers / Lambda の実行時間は外向きリクエストの待ち時間も含めて加算されます。プリフライトで弾けば実行時間は数ミリ秒に収まります
  • 運用上の認知負荷: 自社で防げるエラーが Sentry に流れ込む状態は、本当に見るべきモデル側の障害(529 など)を埋もれさせます。プリフライトを通したリクエストだけが Sentry に到達するようにすれば、通知の信頼度が上がります

ここで私が気をつけたのは「プリフライトは“仮想 API サーバー”として実装する」という意識の置き方でした。Anthropic 側でどんなチェックが行われているかをドキュメントとエラーメッセージから逆引きし、同じ判定をクライアント側で先回りして行います。完璧に再現する必要はなく、Sentry の上位 5 件のエラーパターンを潰せれば、運用ノイズは劇的に下がります。

第1層 — メッセージとツールのスキーマ・バリデーション

最も頻度が高かったのは、messagestools の構造ミスです。role: "user"role: "assistant" を交互に並べる暗黙のルール、tools[].input_schema が JSON Schema である必要があること、空文字列の content を許さないこと ―― このあたりは Anthropic の SDK もある程度ガードしてくれますが、TypeScript の型は実行時には消えるため、ランタイムバリデーションを 1 段挟む必要があります。

ここで使うのは Zod です。型と実行時チェックが 1 つの定義から得られるため、API 境界に置くのに向いています。Zod を使った Claude API ツール呼び出しの型設計の詳細は Claude API のツール呼び出しを Zod で型安全にした実装パターン でも詳しく扱っています。下のコードは、私が実際に本番で使っているスキーマ定義の最小構成です。

// preflight/schema.ts
import { z } from "zod";
 
const TextBlock = z.object({
  type: z.literal("text"),
  text: z.string().min(1, "text content cannot be empty"),
});
 
const ToolUseBlock = z.object({
  type: z.literal("tool_use"),
  id: z.string().regex(/^toolu_/),
  name: z.string().min(1),
  input: z.record(z.unknown()),
});
 
const ToolResultBlock = z.object({
  type: z.literal("tool_result"),
  tool_use_id: z.string().regex(/^toolu_/),
  content: z.union([z.string(), z.array(TextBlock)]),
  is_error: z.boolean().optional(),
});
 
const ContentBlock = z.discriminatedUnion("type", [
  TextBlock,
  ToolUseBlock,
  ToolResultBlock,
]);
 
const Message = z.object({
  role: z.enum(["user", "assistant"]),
  content: z.union([z.string().min(1), z.array(ContentBlock).min(1)]),
});
 
export const RequestSchema = z.object({
  model: z.string(),
  max_tokens: z.number().int().positive(),
  messages: z.array(Message).min(1).refine(
    (msgs) => alternates(msgs),
    "messages must alternate between user and assistant"
  ),
  system: z.union([z.string(), z.array(TextBlock)]).optional(),
  tools: z.array(z.object({
    name: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/),
    description: z.string().min(1),
    input_schema: z.object({
      type: z.literal("object"),
      properties: z.record(z.unknown()),
      required: z.array(z.string()).optional(),
    }),
  })).optional(),
});
 
function alternates(msgs: { role: string }[]): boolean {
  for (let i = 1; i < msgs.length; i++) {
    if (msgs[i].role === msgs[i - 1].role) return false;
  }
  return true;
}
// 実行例: const parsed = RequestSchema.safeParse(req); if (!parsed.success) { ... }

ポイントは、Anthropic のエラーメッセージから逆引きしたバリデーションを Zod の refine で組み立てることです。たとえば messages must alternate のチェックは、私が運用で何度も見たエラーパターンを直接コードに落としたものです。tool_use_idtoolu_ プレフィックスは公式ドキュメントには明記されていない暗黙の規約ですが、実装で観察した事実です。

よく見かけた 400 の原因 5 選

  • messages が空配列(フロント側で空チャットを送ってしまう)
  • role が連続している(要約処理で assistant 同士をマージし忘れた)
  • tool_resulttool_use_id が前ターンの tool_use.id と一致しない
  • tools[].name に空白や日本語が含まれている
  • max_tokens が 0 または負数(環境変数の読み取り漏れ)

第2層 — トークン予算ガードで 413 と 529 を未然に避ける

200K あるいは 1M コンテキストのモデルでも、max_tokens を加えた合計が上限を超えれば context_length_exceeded が返ります。さらに、入力トークンが大きいほど 529(過負荷)の確率が上がる傾向があるため、入力サイズの管理は信頼性に直結します。Anthropic は公式に count_tokens エンドポイントを提供しているので、本番ではこれを利用するのが正攻法ですが、毎回叩くとレイテンシが乗るため、私は「プリフライトでは概算、本番呼び出し前のサンプリング監査で実数」という二段構えにしています。

// preflight/token-budget.ts
import { encoding_for_model, get_encoding } from "@dqbd/tiktoken";
 
// Claude のトークナイザは Anthropic 独自ですが、cl100k 系で
// 概算しても実用上は ±5% に収まります(自社計測値)。
const enc = get_encoding("cl100k_base");
 
interface TokenBudgetResult {
  estimated: number;
  contextLimit: number;
  maxTokens: number;
  ok: boolean;
  reason?: string;
}
 
const MODEL_LIMITS: Record<string, number> = {
  "claude-opus-4-6": 200_000,
  "claude-sonnet-4-6": 200_000,
  "claude-haiku-4-5-20251001": 200_000,
};
 
export function checkTokenBudget(req: {
  model: string;
  max_tokens: number;
  messages: Array<{ role: string; content: unknown }>;
  system?: string;
  tools?: Array<{ description: string; input_schema: unknown }>;
}): TokenBudgetResult {
  const limit = MODEL_LIMITS[req.model] ?? 200_000;
  const serialized =
    (req.system ?? "") +
    JSON.stringify(req.messages) +
    JSON.stringify(req.tools ?? []);
  const inputTokens = enc.encode(serialized).length;
  const estimated = inputTokens + req.max_tokens;
  const headroom = Math.floor(limit * 0.95); // 5% を安全マージンとして残す
 
  if (estimated > headroom) {
    return {
      estimated,
      contextLimit: limit,
      maxTokens: req.max_tokens,
      ok: false,
      reason: `estimated ${estimated} tokens exceeds 95% of context (${headroom})`,
    };
  }
  return { estimated, contextLimit: limit, maxTokens: req.max_tokens, ok: true };
}
// 期待出力: { estimated: 12345, contextLimit: 200000, ok: true }

5% の安全マージンは、cl100k_base による概算が実トークン数より少なめに出る傾向があるための緩衝です。私の環境では、200K モデルで headroom = 190,000 を超えるリクエストはほぼ確実に 413 か 529 のいずれかを引き起こしていました。

第3層 — モデルの機能ケーパビリティチェック

claude-opus-4-6 で動いていたコードを claude-haiku-4-5-20251001 に変えた瞬間に、visionextended_thinking を使ったリクエストが軒並み壊れた経験はないでしょうか。モデルごとに対応している機能セットは微妙に異なり、これを実行時に判定しないと、デプロイ後の数時間でユーザーの目に触れる障害として顕在化します。

ケーパビリティを定数として持ち、リクエスト構築時に matching するパターンが運用上いちばん安全でした。

// preflight/capability.ts
type Feature = "vision" | "tool_use" | "extended_thinking" | "1m_context";
 
const CAPABILITIES: Record<string, Feature[]> = {
  "claude-opus-4-6": ["vision", "tool_use", "extended_thinking"],
  "claude-sonnet-4-6": ["vision", "tool_use", "extended_thinking"],
  "claude-haiku-4-5-20251001": ["tool_use"],
};
 
export function checkCapabilities(req: {
  model: string;
  hasImage: boolean;
  usesTools: boolean;
  thinking?: { type: string };
}): { ok: boolean; missing: Feature[] } {
  const supported = new Set(CAPABILITIES[req.model] ?? []);
  const required: Feature[] = [];
  if (req.hasImage) required.push("vision");
  if (req.usesTools) required.push("tool_use");
  if (req.thinking?.type === "enabled") required.push("extended_thinking");
  const missing = required.filter((f) => !supported.has(f));
  return { ok: missing.length === 0, missing };
}
// 期待出力: { ok: false, missing: ["vision"] } のように
// 不足機能を列挙して返す

このマトリクスは Anthropic のドキュメントが更新されたときに同期するのが運用負荷ですが、半年に1度程度の更新で十分追従できる粒度です。私はモノレポの packages/claude-capabilities に切り出して、4サイトで共有しています。

第4層 — コンテンツポリシーと暗黙のリーク防止

API レイヤで止めるべきは「Anthropic のポリシー違反」だけではありません。自分側のシステムプロンプトをユーザーメッセージに混入させてしまうバグや、PII(個人情報)をそのまま外部 API に送ってしまう問題も、ここで先に検出します。

私が用意したのは「禁止語リスト」と「インジェクション兆候の正規表現」、そして「PII 検出の3ルール」だけのシンプルな実装ですが、これだけで誤送信のヒヤリハットは月 5 件 → 0 件に減りました。

// preflight/content-policy.ts
const SYSTEM_LEAK_MARKERS = [
  /You are a helpful assistant/i,
  /<\|im_start\|>/,
  /BEGIN INTERNAL PROMPT/i,
];
const PII_PATTERNS = [
  /\b\d{3}-\d{4}-\d{4}\b/,         // 日本の電話番号
  /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i, // メール
  /\b\d{4}-\d{4}-\d{4}-\d{4}\b/,    // クレジットカード
];
 
export function checkContentPolicy(req: {
  messages: Array<{ role: string; content: string | unknown[] }>;
}): { ok: boolean; violations: string[] } {
  const v: string[] = [];
  for (const msg of req.messages) {
    const text =
      typeof msg.content === "string"
        ? msg.content
        : msg.content.map((b: any) => b.text ?? "").join("\n");
    for (const re of SYSTEM_LEAK_MARKERS) {
      if (re.test(text)) v.push(`possible system prompt leak: ${re}`);
    }
    for (const re of PII_PATTERNS) {
      if (re.test(text)) v.push(`possible PII: ${re}`);
    }
  }
  return { ok: v.length === 0, violations: v };
}

「PII を検出したら拒否」ではなく「警告とともに匿名化を提案する」運用にしているのが私のチームの方針です。多層防御の考え方そのものや、より高度なインジェクション検出のパターンは Claude API プロンプトインジェクション防御の設計パターン で扱っています。ユーザー体験を壊さずに、開発者へのアラートを残せます。

第5層 — 予算ガードと利用量レート制御

最後の砦は、お金とレートです。日次の自動停止という観点では Claude API の予算上限を確実に守るサーキットブレーカー設計 と組み合わせると、予算超過の検知から呼び出し停止までを一気通貫で構築できます。Anthropic 側にも組織レベルの spend limit はありますが、ユーザー単位・機能単位の予算はサービス側で管理する必要があります。私は Cloudflare KV にユーザーごとの当月利用額を保存し、API 呼び出しの直前に確認しています。

// preflight/budget-guard.ts
interface SpendCap {
  monthlyJpy: number;
  perRequestJpy: number;
}
 
export async function checkBudget(
  kv: KVNamespace,
  userId: string,
  cap: SpendCap,
  estimatedJpy: number
): Promise<{ ok: boolean; reason?: string }> {
  const monthKey = new Date().toISOString().slice(0, 7);
  const used = parseFloat(
    (await kv.get(`spend:${userId}:${monthKey}`)) ?? "0"
  );
  if (used + estimatedJpy > cap.monthlyJpy) {
    return {
      ok: false,
      reason: `monthly cap exceeded: ${used + estimatedJpy} > ${cap.monthlyJpy}`,
    };
  }
  if (estimatedJpy > cap.perRequestJpy) {
    return {
      ok: false,
      reason: `per-request cap exceeded: ${estimatedJpy} > ${cap.perRequestJpy}`,
    };
  }
  return { ok: true };
}
// 期待出力: { ok: false, reason: "monthly cap exceeded: 6200 > 5000" }

estimatedJpy は第2層のトークン推定値とモデル単価を掛け合わせて計算します。ここでの推定は完璧でなくて構いません。「ユーザーが 1 日に出す請求書の上限は1万円」と決めておけば、悪意のあるリクエストで突発的に1万円分を撃たれる事故が防げます。

5層を 1 関数にまとめる — preflight() の最終形

各層を別ファイルに置いておくのは保守性のためですが、本番呼び出し直前は単一の preflight() 関数を経由する設計にします。返却型を「成功」「拒否(理由付き)」の sum type にしておくと、呼び出し側は if (result.kind === "deny") で分岐するだけで済みます。

// preflight/index.ts
import { RequestSchema } from "./schema";
import { checkTokenBudget } from "./token-budget";
import { checkCapabilities } from "./capability";
import { checkContentPolicy } from "./content-policy";
import { checkBudget } from "./budget-guard";
 
export type PreflightResult =
  | { kind: "allow"; estimated: { tokens: number; jpy: number } }
  | { kind: "deny"; layer: 1 | 2 | 3 | 4 | 5; reason: string };
 
export async function preflight(
  req: unknown,
  ctx: { kv: KVNamespace; userId: string; cap: SpendCap }
): Promise<PreflightResult> {
  const parsed = RequestSchema.safeParse(req);
  if (!parsed.success) {
    return { kind: "deny", layer: 1, reason: parsed.error.message };
  }
  const r = parsed.data;
 
  const tb = checkTokenBudget(r);
  if (!tb.ok) return { kind: "deny", layer: 2, reason: tb.reason! };
 
  const cap = checkCapabilities({
    model: r.model,
    hasImage: detectImage(r.messages),
    usesTools: !!r.tools?.length,
  });
  if (!cap.ok) {
    return { kind: "deny", layer: 3, reason: `missing: ${cap.missing}` };
  }
 
  const cp = checkContentPolicy(r);
  if (!cp.ok) {
    return { kind: "deny", layer: 4, reason: cp.violations.join("; ") };
  }
 
  const estimatedJpy = estimateJpy(r.model, tb.estimated);
  const bg = await checkBudget(ctx.kv, ctx.userId, ctx.cap, estimatedJpy);
  if (!bg.ok) return { kind: "deny", layer: 5, reason: bg.reason! };
 
  return {
    kind: "allow",
    estimated: { tokens: tb.estimated, jpy: estimatedJpy },
  };
}

返却型に layer を入れているのは観測のためです。Sentry には tag: layer-2 のような形でラベルを付け、どの層が一番拒否を出しているかを Datadog の時系列で追えるようにしています。第3層の拒否が増えてきたらモデル切り替えの不具合、第5層が増えたら予算設計の見直しサイン、というように、層自体が運用上のシグナルになります。

1 ヶ月運用してみて見えた数値

導入前後の1ヶ月(同じ呼び出し量)で比較した結果はこちらです。数字はあくまで個人の運用環境のものですが、傾向の参考になれば嬉しいです。

  • invalid_request_error: 412 件 → 0 件(プリフライト第1〜3層が捕捉)
  • overloaded_error 通知: 89 件 → 41 件(第2層のトークン削減効果)
  • 失敗呼び出しに伴う Cloudflare Workers 実行時間の合計: 約 18 分 → 約 30 秒
  • Sentry の月額負荷(Errors quota): 平均 60% 消費 → 12% 消費
  • 新機能リリース時の本番事故: 月 2 件 → 0 件(ケーパビリティチェックで事前検出)

最も効いたのは第1層と第3層でした。第3層は本番事故そのものを止めるレイヤなので、入れた瞬間に効果が出ます。第1層は積もり積もったノイズを一気に消すレイヤなので、運用の精神衛生に直結します。

私が踏んだ3つの落とし穴

完璧な設計に見えるかもしれませんが、導入時に私自身が踏んだ罠もお伝えしておきます。

  • 過剰検証で正常リクエストを落とす: 最初は tools[].name の正規表現をきつくしすぎて、- を含む既存ツールが全部落ちました。本番投入前に直近 7 日のリクエストログを通して試走するテストを必ず挟むこと
  • 検証コードと本番ロジックの乖離: フロントエンドのバリデーションと API 境界のプリフライトで、別々の Zod スキーマを書いてしまうと必ず腐ります。@anthropic-types/preflight のような共有パッケージに切り出すのが正解
  • トークン推定の過信: cl100k_base ベースの推定は、長文の日本語で誤差が大きくなります。月1回ほどは公式の count_tokens エンドポイントでサンプリング監査をして、自社推定値の補正係数を更新するのを忘れないでください

次の一歩 — 第1層だけでも今日入れる

5層をいきなり全部入れるのは負担が大きいので、まずは第1層の Zod スキーマだけを API ハンドラに差し込んでみてください。それだけで「Anthropic に届く前のリクエスト」と「実際に届いたリクエスト」の差分が可視化され、Sentry の通知が確実に減ります。私自身、最初は第1層だけで運用していました。第2層〜第5層は、運用しながら必要に応じて重ねていけば十分です。

私自身、まだトークン推定の精度向上や、第6層(呼び出しパターンの異常検知)の追加を模索中です。同じ景色を共有している方の参考になれば嬉しいです。お読みいただき、ありがとうございました。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API & SDK2026-06-28
弱いモデルへ静かに落ちる方がエラーより怖い — Claude API のモデル降格に「下限」を設ける設計
無人運用のパイプラインでモデルが使えなくなったとき、弱いモデルへ自動で落ちる実装は危険です。タスクごとの能力契約と降格予算で「どこまで落ちたら止めるか」を決める設計を、個人開発の自動運用の実体験から解説します。
API & SDK2026-05-26
Claude API の構造化レスポンスを本番で安定させる — tool_use と JSON Schema、多層検証の実装メモ
Claude API の応答を JSON で受け取る実装は数行で書けますが、本番運用では「形は正しいが意味が壊れている」レスポンスにどう備えるかが分かれ目になります。tool_use・JSON Schema・ドメイン検証を組み合わせた多層パイプラインを、壁紙アプリの分類処理での実体験を交えて整理しました。
API & SDK2026-05-22
Claude API の tool_result could not be submitted が再発しないための回復ハンドラ設計
Claude API でエージェントを長時間動かしていると、ある日突然 'tool_result could not be submitted' が連発し始めることがあります。再試行しても直らない、ストリーミング途中で死ぬ、そして次のセッションでもまた出る — 個人開発で運営している6サイトの自動投稿パイプラインで実際に踏み抜いた4種類の原因と、TypeScript で書いた回復ハンドラの実装を整理しました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →