HTTP エラー トリアージ
HTTP エラーはリクエストが失敗したことを示します。しかし、通常は なぜ 失敗したのかは教えてくれません。 403 を見て「キーが無効だ」と結論づけるのは、数字を着せた推測にすぎません。
実在のインシデント(2026-07-30)に端を発しています。あるサードパーティの連絡先エンリッチメントAPIが、
6日間すべての呼び出しに対して 403 error code: 1010 を返しました。これは「キーに権限がない」、「クレジット切れ」、
「月間クォータに達した」と解釈されました。その思い込みに基づいて756行のブラウザ自動化回避策が構築され、
顧客には正常なプランを確認するよう依頼するところでした。実際の原因は User-Agent の欠落で、CDNがエッジでクライアントを
禁止したのです。1つのヘッダで修正されました:200 OK、完全なデータ、そしてクレジット残高はずっとそこにありました。
診断前に実行すべき3つの制御テスト
1. 無効な資格情報の制御テスト。 同じリクエストを、意図的に無効な資格情報と、資格情報なしで送信します。
for label, key in [("real", REAL_KEY), ("garbage", "0"*36), ("empty", "")]:
... # 同じURL、同じメソッド、同じヘッダ
3つすべてが 同じ ステータスとボディを返した場合、エラーは資格情報に関するものではありません。 権限、クォータ、有効性についての情報はまったくありません。キーについての推論をやめてください。
2. 無意味なパスの制御テスト。 確実に存在しないパスと、サイトルートをリクエストします。
GET https://api.example.com/definitely-not-a-real-path-12345
GET https://api.example.com/
GET https://www.example.com/ # マーケティングサイト
APIエラーがマーケティングサイトからも返ってきた場合、オリジンの 手前 でブロックされています。 API呼び出しについての情報は何も役に立ちません。
3. レスポンスヘッダを読む(ボディだけでなく)。
except urllib.error.HTTPError as e:
print(e.code, e.headers.get("server"), e.read()[:200])
「auth」エラーで server: cloudflare は決定的な手がかりです。cf-ray、x-amzn-waf、akamai も同様です。
これらはインフラストラクチャであり、APIではありません。
既知のシグネチャ
| 症状 | 実際の原因 | 修正方法 |
|---|---|---|
403 + ボディ error code: 1010、server: cloudflare | Cloudflareがクライアント署名を禁止 | 実際のブラウザの User-Agent を送信 |
すべてのパス(/ を含む)で同じエラー | エッジ/WAFブロックまたはIP禁止 | UAを変更し、次にIP/プロキシを変更 |
402 | 本当にクレジット切れ:リクエストは 認証された | チャージアップ;キーは正常 |
429(成功の直後) | 本当のレート制限 | リクエストを調整し、バックオフを追加 |
400(フィールドメッセージあり) | APIに到達し、認証が通った | ペイロードを修正;これは良い知らせ |
400 バリデーションエラーはトリアージにとって 成功のシグナル です:認証が受け入れられたことを証明します。
サーバーサイドAPI呼び出しのデフォルトヘッダ
Python の urllib は User-Agent: Python-urllib/3.x を送信しますが、これは多くのWAFが即座に禁止します。
常に以下を設定してください:
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36",
"Accept": "application/json",
"Content-Type": "application/json",
}
その後、一括処理の前にコストを測定する
動作するようになったら、支出を推定しないでください。1回の呼び出しの前後に使用量を読み取り、プロバイダ自身の課金フィールドをレスポンスで確認します。
before = usage(); result = call(); after = usage()
print("charged:", result.get("billing"), "| measured:", before - after)
報告すべきこと
テストしたことと、まだ不明なことを述べてください。「両方のキーで403」は、空のキーでも同じ403が返ってくる場合、発見とは言えません。代わりにこう言いましょう:「ここでの403は情報量がなく、nullの資格情報でも同じエラーが返ってきます。原因は認証より上流にあります。」