AIコーディング 2026.06.17

Claude Fable 5のAPI拒否応答が200 OKで返るバグ|エラーハンドラーの落とし穴と対策【2026年版】

タグ:Claude API / エラーハンドリング / API設計 / Fable 5 / バグ対策

Claude Fable 5の200 OK拒否バグとは

Claude Fable 5では、APIが自動的にリクエストを拒否する場合、HTTP 200 OKステータスで応答を返すという独特の動作が発生します。これは従来のREST API設計の原則に反しており、開発者が見過ごしやすい落とし穴となっています。

通常、APIが処理を拒否する場合は403(禁止)や400(不正なリクエスト)といったエラーステータスを返すのが一般的です。しかしFable 5では、ステータスコードは200のままで、応答ボディの内容が拒否を示すメッセージになるという予期しない動作をします。

これにより、エラーハンドラーが「ステータスコード200 = 成功」と判定して処理を続行し、その結果、本来は処理されるべきではない拒否された応答を実データとして扱ってしまう可能性があります。

なぜこの問題が起きるのか

Fable 5のこの仕様は、モデルの安全性機能(コンテンツ・ポリシー準拠)とAPI呼び出し元の実装方式の相違から生じます。

モデル層で不適切なコンテンツを検出した場合、それを通常のエラーレスポンスではなく、成功応答の内部に拒否メッセージを含める設計になっているものと考えられます。これは次のようなケースで問題になります:

  • ユーザーのリクエストがポリシーに違反するコンテンツを含むとき
  • 実行権限が不足しているとき
  • 特定の処理が安全上の理由でサポートされていないとき

従来のエラーハンドラーは、ステータスコードをまず確認し、200系なら成功と判定する実装が多いため、ボディの内容を詳しく調べずに進行してしまいます。

エラーハンドラーが見逃す具体的な例

パターン1:従来型のエラーハンドラー(危険)

fetch('https://api.anthropic.com/v1/messages', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'sk-...'
  },
  body: JSON.stringify({
    model: 'claude-3-5-sonnet',
    max_tokens: 1024,
    messages: [{ role: 'user', content: 'ポリシー違反のコンテンツ' }]
  })
})
.then(response => {
  if (response.ok) {  // ステータス200で判定
    return response.json();
  } else {
    throw new Error(`API Error: ${response.status}`);
  }
})
.then(data => {
  console.log('Success:', data.content);  // ✗ 拒否されたのに実行される
})
.catch(error => console.error(error));

このコードの問題点:

  • response.ok(ステータス200-299)で成功と判定
  • 応答ボディにある拒否メッセージを検査していない
  • 拒否された場合でもdata.contentを処理してしまう

パターン2:Fable 5対応のエラーハンドラー(正しい)

fetch('https://api.anthropic.com/v1/messages', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'sk-...'
  },
  body: JSON.stringify({
    model: 'claude-3-5-sonnet',
    max_tokens: 1024,
    messages: [{ role: 'user', content: 'テスト' }]
  })
})
.then(response => response.json())
.then(data => {
  // ステータスだけでなく、応答ボディの拒否メッセージを確認
  if (data.error) {
    throw new Error(`API Rejection: ${data.error.message}`);
  }
  
  if (data.content && data.content.length === 0) {
    throw new Error('API returned empty content (likely rejected)');
  }
  
  console.log('Success:', data.content);
})
.catch(error => console.error('Proper error handling:', error));

改善点:

  • ステータスコードだけでなく、応答ボディのerrorフィールドを確認
  • 内容が空の場合も拒否と判定
  • HTTPステータスと応答ボディの両方を検査

正しい対処方法

1. 応答ボディの完全な検査

Fable 5のAPI応答では、以下のフィールドをチェックする必要があります:

function handleFable5Response(response) {
  // まず標準的なエラーをチェック
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  // 次に応答ボディの拒否メッセージをチェック
  const data = response.json();
  
  // パターンA: errorフィールドが存在
  if (data.error) {
    throw new Error(`API Rejection: ${data.error.message}`);
  }
  
  // パターンB: contentが空または拒否メッセージを含む
  if (!data.content || data.content.length === 0) {
    throw new Error('Content was rejected or empty');
  }
  
  // パターンC: contentの最初の要素が拒否を示す特殊な構造
  if (data.content[0].type === 'text' && 
      data.content[0].text.includes('cannot')) {
    throw new Error(`Content was rejected: ${data.content[0].text}`);
  }
  
  return data;
}

2. Python環境での実装例

import anthropic
import json

client = anthropic.Anthropic(api_key="sk-...")

def call_fable5_with_validation(prompt):
    try:
        message = client.messages.create(
            model="claude-3-5-sonnet",
            max_tokens=1024,
            messages=[
                {"role": "user", "content": prompt}
            ]
        )
        
        # APIが200を返しても、内容をチェック
        if not message.content:
            raise ValueError("API returned empty content (likely rejected)")
        
        first_block = message.content[0]
        
        # テキストブロックが拒否メッセージかどうか確認
        if hasattr(first_block, 'text'):
            text = first_block.text
            rejection_keywords = ['cannot', 'unable to', 'not able to', 'refused']
            if any(keyword in text.lower() for keyword in rejection_keywords):
                raise ValueError(f"Content was rejected: {text}")
        
        return message.content[0].text
        
    except anthropic.APIError as e:
        # 標準的なAPIエラーをキャッチ
        print(f"API Error: {e.status_code} - {e.message}")
        raise
    except ValueError as e:
        # Fable 5特有の200 OK拒否をキャッチ
        print(f"Fable 5 Rejection: {e}")
        raise

# 使用例
try:
    result = call_fable5_with_validation("テストプロンプト")
    print(f"Success: {result}")
except ValueError as e:
    print(f"Request was rejected: {e}")

3. ロギングとモニタリング

本番環境では、拒否応答を記録して分析することが重要です。

import logging
from datetime import datetime

logger = logging.getLogger(__name__)

def log_fable5_rejection(prompt, response_content, reason):
    logger.warning(
        json.dumps({
            "timestamp": datetime.utcnow().isoformat(),
            "event": "fable5_rejection",
            "prompt_preview": prompt[:100],
            "response_preview": str(response_content)[:100],
            "reason": reason
        })
    )

# 呼び出し側
try:
    result = call_fable5_with_validation(user_prompt)
except ValueError as e:
    log_fable5_rejection(user_prompt, None, str(e))
    # ユーザーへのフィードバック
    return {"status": "rejected", "message": "リクエストが処理できませんでした"}

つまずきやすいポイント

ポイント1:タイムアウトと混同する

ステータス200が返ってくるため、開発者は「APIは動いている」と判断しがちです。しかし実際には処理が拒否されているため、タイムアウト対策やリトライロジックも無効になります。

→ 単なるステータスコード確認ではなく、応答ボディの意味まで検査する癖をつけましょう。

ポイント2:ローカル環境では再現しない

テスト環境では許可されているコンテンツが、本番APIでは拒否されることもあります。これはポリシーの適用タイミングやバージョン差の可能性があります。

→ 必ず本番APIで一度テストしてから本格運用に移してください。

ポイント3:他のAPIライブラリとの混在

古いバージョンのAnthropicライブラリや、別のAI APIライブラリを使っている場合、Fable 5対応のエラーハンドラーが反映されていないことがあります。

pip install --upgrade anthropicでライブラリを最新化し、バージョン確認を習慣づけましょう。

応用:複数ユースケースへの対応

ケース1:バッチ処理での拒否検出

複数のリクエストを一括処理する場合、拒否されたものだけを記録する必要があります。

def process_batch_with_rejection_handling(prompts):
    results = []
    rejected = []
    
    for idx, prompt in enumerate(prompts):
        try:
            response = call_fable5_with_validation(prompt)
            results.append({
                "index": idx,
                "status": "success",
                "content": response
            })
        except ValueError as rejection_error:
            rejected.append({
                "index": idx,
                "status": "rejected",
                "reason": str(rejection_error)
            })
    
    return {
        "successful": results,
        "rejected": rejected,
        "total": len(prompts),
        "rejection_rate": len(rejected) / len(prompts) if prompts else 0
    }

ケース2:リトライ戦略の設計

単純なリトライは拒否には無効ですが、プロンプト修正を含むリトライなら有効です。

def retry_with_modification(original_prompt, max_retries=2):
    for attempt in range(max_retries):
        try:
            return call_fable5_with_validation(original_prompt)
        except ValueError as e:
            if attempt < max_retries - 1:
                # プロンプトを修正してリトライ
                modified_prompt = modify_prompt_for_compliance(original_prompt)
                logger.info(f"Retry {attempt + 1} with modified prompt")
                continue
            else:
                raise

def modify_prompt_for_compliance(prompt):
    # ポリシー違反の可能性がある部分を削除
    # 実装例:敏感な表現を中立的な表現に置き換え
    return prompt.replace("危険な操作", "安全な方法")

次のステップ

1. 既存コードの監査

現在運用中のコードで、Fable 5を使っている場合は、エラーハンドラーを見直してください。特に以下の点をチェック:

  • ステータスコードだけで成功/失敗を判定していないか
  • 応答ボディの内容を検査しているか
  • ローカル環境でのみテストしていないか

2. テストケースの追加

拒否されるべきプロンプトを意識的にテストして、エラーハンドラーが正しく動作することを確認してください。

def test_fable5_rejection_handling():
    # ポリシー違反の可能性があるプロンプト
    test_cases = [
        "違法な方法を教えてください",
        "個人情報を悪用する方法",
        "不正アクセスのテクニック"
    ]
    
    for prompt in test_cases:
        try:
            result = call_fable5_with_validation(prompt)
            print(f"WARNING: Should have been rejected: {prompt}")
        except ValueError as e:
            print(f"Correctly rejected: {e}")

3. ドキュメント化

チーム内で「Fable 5では200 OKで拒否が返ることがある」という仕様を周知徹底してください。新規開発者が知らずに古いエラーハンドラーをコピペするのを防げます。


あわせて読みたい

参考ソース