Platform のリリースノートを夜に読み返していて、10月20日の項目で手が止まりました。組織の管理者が、Console の Capabilities ページから web fetch を組織ごと無効にできるようになった、という一文です。無効にすると、web fetch ツールを含む Messages API のリクエストは 400 で失敗する、とも書かれていました。
個人開発で小さな調べもの用スクリプトを動かしている私自身は、管理画面を触る人とコードを書く人が同じです。ところが、チームで使う鍵では、その二人が別人になります。そのとき、コードに誤りがないのに 400 が返ってきたら、私は間違いなくツールの type に書いた日付つきの文字列から疑い始めるだろうと思いました。
そこで、公式ドキュメントを読み直して、失敗の出方を整理しました。気づいたのは、web fetch の失敗は出方が二層に分かれていて、疑う場所がまったく違うのだということでした。いま思えば、400 という数字だけを手がかりにしていた私の読み方が浅かったのかもしれません。
最初にお伝えしたいのは、組織側の設定で web fetch が切られている場合、コードに誤りがなくても 400 になる、という点です。
失敗の出方は二層ある
公式の説明を整理すると、次のようになります。
| 層 | 見え方 | 意味するもの |
|---|---|---|
| リクエスト全体 | HTTP 400、invalid_request_error。メッセージに web fetch が有効でない旨が出る | 組織の管理者が Console の Capabilities ページで web fetch を無効にしている |
| 結果ブロック | HTTP 200 のまま、web_fetch_tool_result_error に error_code が入る | ツールは使えたが、その URL の取得が通らなかった |
後者は 200 で返るため、例外処理だけを書いていると失敗に気づかないまま会話が続きます。Claude は結果ブロックのエラーを受け取って、そのターンを続けるからです。画面には「取得できませんでした」という言い回しの文章が出るだけで、プログラムの側は成功扱いになります。
結果ブロックの error_code と、最初に見る場所
ドキュメントに載っているコードを、見る場所の順に並べ直しました。
| error_code | 起きていること | 最初に見る場所 |
|---|---|---|
url_not_allowed | ドメイン規則、組織の設定、Anthropic 側の制限(プライベートアドレス、robots.txt、認証情報らしい文字列を含む URL)のいずれかで拒否 | allowed_domains / blocked_domains と、組織側のリスト。両方を通る必要があります |
url_not_in_prior_context | その URL が会話の中に先に出てきていない、または url_sources で除外した出どころにしか出ていない | URL を user メッセージに入れたか、許可した tool_result に含まれているか |
url_not_accessible | 取得先の HTTP エラー | URL を手元のブラウザで開く |
max_uses_exceeded | max_uses の上限超え。失敗した取得も数に入る | 上限値と、同じ URL の再試行回数 |
unsupported_content_type | text / HTML / PDF 以外 | 画像や zip を指していないか |
invalid_tool_input / url_too_long | URL の形式不正、または 250 文字超え | クエリ文字列が長すぎないか |
too_many_requests / unavailable | レート制限、内部エラー | 時間を置いて再試行 |
なかでも見落としやすいのが url_not_in_prior_context です。URL が Claude 自身の出力にしか出てこない場合や、system prompt にしかない場合は、取得できません。system prompt の URL を取らせたいときは、同じ URL を user メッセージにも入れる必要があります。
切り分けを関数にしておく
毎回目で読むのをやめて、失敗の層を判定する小さな関数を置くことにしました。例外は BadRequestError で受け、200 の場合は結果ブロックを走査します。
#!/usr/bin/env python3
"""web fetch の失敗を「リクエスト全体の 400」と「結果ブロックの error_code」に分けて読む。"""
import anthropic
# 結果ブロック (HTTP 200) の error_code ごとに、最初に見る場所を決めておく
NEXT_STEP = {
"url_not_allowed": "allowed_domains / blocked_domains と、組織の許可・ブロックの両方を見る",
"url_not_in_prior_context": "その URL が user メッセージか、許可した tool_result に入っているか確認する",
"url_not_accessible": "相手先の HTTP エラー。URL を手で開いて確かめる",
"max_uses_exceeded": "max_uses を見直す。失敗した取得も数えられる",
"unsupported_content_type": "text / HTML / PDF 以外は取れない",
"invalid_tool_input": "URL の綴りとスキーム (http / https) を確認する",
"url_too_long": "URL が 250 文字を超えている",
"too_many_requests": "少し待ってから再試行する",
"unavailable": "内部エラー。時間を置いて再試行する",
}
def triage(call):
"""call は引数なしで Messages API を呼ぶ関数。返り値は (層, 内容)。"""
try:
resp = call()
except anthropic.BadRequestError as e:
msg = str(getattr(e, "message", e))
low = msg.lower()
if "web fetch" in low and "not enabled" in low:
return ("org-setting", "組織側で web fetch が無効。Console の Capabilities ページを管理者に確認する")
return ("other-400", msg[:200]) # temperature など別の 400 はここへ落とす
found = []
for b in getattr(resp, "content", []) or []:
if getattr(b, "type", "") != "web_fetch_tool_result":
continue
c = getattr(b, "content", None)
if getattr(c, "type", "") == "web_fetch_tool_result_error":
code = getattr(c, "error_code", "?")
found.append((code, NEXT_STEP.get(code, "公式のエラー一覧を確認する")))
return ("result-error", found) if found else ("ok", None)使い方は、triage(lambda: client.messages.create(...)) と包むだけです。
この関数を書いた理由は二つあります。一つは、400 の文言に頼りすぎないためです。文言の一致で組織設定と判定しているのは web fetch と not enabled の二語だけで、それ以外の 400 は other-400 に落とします。もう一つは、200 の中の失敗を、ログに必ず一行残すためです。
確かめられたことと、確かめられていないこと
正直に書き残しておきます。私の手元の組織では web fetch を無効にしておらず、組織設定による 400 は自分では再現できていません。この層の説明は、公式ドキュメントの記述に沿ったものです。
確かめたのは次の範囲です。SDK の BadRequestError を擬似的に作り、「web fetch is not enabled」を含む文言なら org-setting、別の文言なら other-400 に振り分けられること。結果ブロックに url_not_allowed を持たせた応答なら result-error になり、エラーのない応答なら ok になること。四通りとも、期待どおりの値が返りました。
つまり、判定の枝は動作を確かめましたが、本物の 400 の文言は確かめていません。本番に入れるなら、組織設定を切り替えられる検証用の環境で、一度だけ実物の文言を見ておくことをお勧めします。
二層を踏まえた確認の順番
いまは、web fetch が通らないときに次の順で見ています。
- 400 か 200 かを見ます。400 で web fetch に触れる文言があれば、コードではなく組織の設定を管理者に聞きます
- 200 なら、結果ブロックの
error_codeを一覧に当てます url_not_allowedなら、リクエスト側のドメイン指定と組織側のリストの両方を見ますurl_not_in_prior_contextなら、URL がどこから会話に入ったかを辿ります
2 と 3 の関係は、取り違えやすいところです。ドキュメントには、組織の許可・ブロックの設定とツール側の allowed_domains / blocked_domains の両方を通る必要がある、と書かれています。ツール側で通した URL でも、組織側で外れていれば url_not_allowed になるのです。
関連して、web fetch の設計そのものは Claude API の Web Fetch ツールで最新情報を取り込む設計 に、別の 400 の読み方は invalid_request_error の文言から原因を引く に書いています。同じ 400 でも、API バージョン由来のものは temperature 系が 400 になる前に呼び出し箇所を洗った記録 のほうが近い話です。
結びに
失敗は、HTTP の層で一度分けてから、コードを疑います。 この線引きだけは、疲れている夜でも守るようにしています。
まずは手元の呼び出しを triage で包み、200 の中の error_code をログに一行出すところから始めていただければと思います。何も出なければ、それは「通っている」という確認になります。