Claude SDKのミドルウェアで共通処理を挟む — 登録順序とHTTPクライアント差し替え
Anthropic公式SDKのミドルウェアでログやリトライなど横断処理を挟む方法。登録順序、HTTPクライアントの差し替え、組み込みのrefusal-fallbackまで扱います。
Claude SDKのミドルウェアで何ができるか
Anthropic公式SDK(Python、TypeScript、Go、Java、C#、PHP、Ruby)は、リクエストを送る前とレスポンスを受け取った後にコードを挟めるミドルウェア(インターセプター)機構を備えています。ログ出力、独自のリトライ、リクエストへの注釈付け、拒否応答時のフォールバック処理といった横断的な処理を、API呼び出しのたびに書かずに1か所へ集約できます。
仕組みはシンプルです。各ミドルウェアは「リクエストを受け取り、next() を呼んで次の処理へ渡し、返ってきたレスポンスを加工して返す」関数として書きます。next() を呼ぶ前のコードは往路で、呼んだ後のコードは復路で実行されます。呼び出し元のコード → ミドルウェアA → ミドルウェアB → SDKコア → Claude APIという順で往路が進み、APIからの応答はまったく逆の経路を復路として戻ってきます。この対称性のおかげで、往路で付けた注釈やタイムスタンプを復路側で参照する、といった実装が素直に書けます。
Pythonでの最小構成は次のとおりです。
def logging_middleware(request, call_next):
# 往路: リクエストを送る前
print(f"-> {request.method} {request.url}")
response = call_next(request)
# 復路: レスポンスを受け取った後
print(f"<- {response.status_code}")
return response
client = Anthropic(middleware=[logging_middleware])TypeScriptでは next が非同期になる点だけ異なります。
const loggingMiddleware: Middleware = async (request, next, ctx) => {
ctx.logger.debug("->", request.method, request.url);
const response = await next(request);
ctx.logger.debug("<-", response.status, request.url);
return response;
};
const client = new Anthropic({ middleware: [loggingMiddleware] });Go・Java・C#・PHP・Rubyにも同じ形のフックがあります。関数の型は言語ごとに違っても、「next の前後にコードを挟む」という発想は共通です。ログ出力のほかにも、リクエストへの独自ヘッダー付与、レスポンス受信までの時間計測、社内のトレーシング基盤へのスパン送出など、呼び出し側のビジネスロジックとは独立した処理を差し込む用途に向いています。
複数のミドルウェアを登録する順序のルール
ミドルウェアを配列で複数渡すと、登録順の先頭が最も外側になります。先頭のミドルウェアの往路コードが最初に走り、復路コードが最後に走る、入れ子構造です。クライアント生成時に渡したミドルウェアは、リクエストごとのオプションとして渡したミドルウェアより外側で実行されます。
登録順序が決まると、どのミドルウェアが何を観測できるかも決まります。次節で、ログ取得とリトライを組み合わせた具体例を見ていきます。
複数のミドルウェアを組み合わせた実行順の例
ログ取得とリトライを別々のミドルウェアとして書き、両方を登録するケースを見てみます。
client = Anthropic(middleware=[logging_middleware, retry_middleware])このとき実行順は次のようになります。
logging_middlewareの往路(リクエスト送信前のログ)retry_middlewareの往路- SDKコアがHTTPリクエストを送信(失敗すれば
retry_middlewareの中で再試行) retry_middlewareの復路(最終的に成功したレスポンスを受け取る)logging_middlewareの復路(最終レスポンスのログ)
logging_middlewareが最も外側にあるため、内側のretry_middlewareが何度再試行しても、ログには「最初の送信」と「最終的な結果」だけが1回ずつ記録されます。再試行の回数までログに残したいなら、順序を逆にしてretry側を外側に置きます。どちらが正しいかは要件次第で、ミドルウェアの並び順を変えるだけで観測範囲を切り替えられるのがこの機構の利点です。トークンコストまで含めて観測したい場合は、Prompt Cachingの仕組みを踏まえてキャッシュ命中率もあわせてログに残すと、リトライ発生時の挙動まで見通せます。
HTTPクライアントを丸ごと差し替える
ミドルウェアとは別に、各SDKはHTTPクライアント自体の差し替えも受け付けます。社内プロキシを経由させたい、TLS証明書を独自に指定したい、コネクションプーリングを調整したいといった要件は、ミドルウェアではなくこちらで解決します。
差し替えられるHTTPクライアントは1クライアントにつき1つだけで、指定すると既定のクライアントを置き換えます。カスタムHTTPクライアントは、すべてのミドルウェアを通過した後のリクエストを受け取ります。つまり実行順は「ミドルウェアチェーン → カスタムHTTPクライアント → 実際のネットワーク送信」です。ミドルウェアで加工したヘッダーやURLは、そのままカスタムクライアントに渡ります。
役割の切り分けとしては、「アプリケーションロジックに近い横断処理」はミドルウェア、「通信そのものの土台」はHTTPクライアントの差し替えです。社内プロキシ経由でしか外部通信を許可していない環境では、まずHTTPクライアントの差し替えから検討します。
組み込みミドルウェア: refusal-fallbackで拒否応答を自動リトライする
各SDKには、拒否応答(stop_reason: "refusal")を検知して別モデルに自動で再送する refusal-fallback ミドルウェアが標準で入っています。Claude Fable 5やClaude Fable 5.1が安全分類器によってリクエストを拒否した場合、これは通常のレスポンスとして返ってくるためHTTPエラーでは検知できません。組み込みミドルウェアはこの stop_reason を見て、指定したフォールバックモデルへ同じリクエストを再送します。
from anthropic import Anthropic, BetaFallbackState, BetaRefusalFallbackMiddleware
client = Anthropic(
middleware=[BetaRefusalFallbackMiddleware([{"model": "claude-opus-4-8"}])],
)
state = BetaFallbackState() # 以降の会話をフォールバック先に固定する
with state:
message = client.beta.messages.create(
max_tokens=1024,
model="claude-fable-5",
messages=[{"role": "user", "content": "Hello, Claude"}],
)
print(f"served by: {message.model}")BetaFallbackState を会話内で使い回すと、一度フォールバック先に切り替わったモデルにその後の会話も固定されます。都度ステートを作り直すと、毎ターン最初のモデルへ再送を試みてしまい、拒否が続く会話ではリトライ回数が余計に増えます。
ストリーミングでも仕組みは同じです。拒否が発生すると、ミドルウェアがフォールバックモデルのイベントを同じストリームへ継ぎ足します。呼び出し側は「ストリームが最後まで流れてきた」以上のハンドリングを書く必要がありません。ただし、クライアント側・サーバー側・MCPいずれかのツール呼び出しブロックがまだ開いたままの状態で拒否が発生した場合だけは例外で、フォールバックは行われず拒否がそのままストリームに返ります。fallback-credit-2026-07-01ヘッダーを付けていれば、この場合でも部分応答の続きから再開できるクレジットトークンは付与されます。また、一度フォールバックした会話をその後も同じモデルへ直接ルーティングするsticky routingもストリーミングに適用され、この場合はストリームが開く前にルーティング先が決まっているためmessage_startイベントのmodelフィールドには最初からフォールバック先のモデルIDが入ります。このsticky routingは会話プレフィックスのハッシュ値として概ね1時間だけ保持されるベストエフォートの仕組みで、保持期間を過ぎれば同じ会話でも最初に指定したモデルへ再び送られることがあります。呼び出し側のコードは、フォールバック済みのはずの会話が元のモデルへ戻ってくる可能性を前提に書く必要があります。
フォールバックリストは順番に試され、途中のモデルも拒否すればさらに次のモデルへ進みます。全モデルが拒否した場合は、エラーを投げるのではなく最後のモデルの拒否レスポンスをそのまま返します。またこのミドルウェアは、コストを二重取りしないための fallback-credit-2026-07-01 ベータヘッダーを自動で付与します。
ミドルウェアを使うときによくあるつまずき
- サブエージェント呼び出しにフォールバックが伝わらない。ツール実行の内部から発行するモデル呼び出しは、親リクエストの
fallbacksを自動では引き継ぎません。サブエージェント側のクライアントにも個別にミドルウェアを登録します。 - 同じモデルに再送してしまう。拒否されたリクエストを同じモデルへ投げ直しても、多くの場合また拒否されます。フォールバック先には別モデルを指定します。
- リトライの予算をセッション単位にしてしまう。1ターンの中でエージェントとサブエージェントの両方が拒否を受けることがあるため、リトライ回数はリクエスト単位で管理します。
- 共有フラグでON/OFFを切り替える。グローバル変数やキャッシュされた設定値でミドルウェアの有効・無効を管理すると、更新漏れでいつの間にか無効化された状態が残ります。有効かどうか確認できないなら、明示的にミドルウェアを構成し直すほうが安全です。
- リトライハンドラだけにフォールバックを入れて満足する。エラー復旧の分岐やバックグラウンドワーカーなど、リクエストを再発行するすべての経路にミドルウェアを適用しないと、保護が最も必要な経路だけ素通りします。
- 拒否をエラー監視の指標に含めない。拒否応答はHTTP 200で返るため、エラー率や5xxを見張る監視では検知できません。拒否そのものと、フォールバックで救済できた件数を別々に計測し、両者の差を監視します。判定は
contentやstop_detailsの内側のフィールドではなく、stop_reasonが"refusal"かどうかで行います。
これらはAnthropic公式ドキュメントが「よくある落とし穴」として挙げている項目で、ミドルウェア自体の実装ミスというより、フォールバックの適用範囲を勘違いすることで起きます。
まとめ
ミドルウェアは、ログ・リトライ・リクエスト注釈・拒否応答対応のような横断処理を、呼び出しごとに書かずに1か所へ集約する仕組みです。複数登録するときは先頭が最も外側という順序ルールを踏まえ、プロキシやTLSの調整はミドルウェアではなくHTTPクライアントの差し替えで行います。組み込みの refusal-fallback ミドルウェアは、拒否応答が返る構成(エージェント経由の呼び出しやマルチターン会話)ではまず有効化を検討する価値があります。Anthropic APIの実装を広く見渡したい場合はAnthropic API完全ガイドを参照してください。