Proxy requestのリトライ上限到達の原因

問題

POST /api/proxy/の呼び出しでエラーが返され、データが取得できません。メッセージは短く、常に同じ形式です:

{
  "error": "Download maxTry limit reached",
  "total": 34.812,
  "request": { "...": "..." }
}

すべてのExitがブロックされた場合でも、すべてのExitが停止していた場合でも、あるいはFourAがほぼすべての試行で実際のページを取得したにもかかわらず独自のvalidateルールによって破棄された場合でも、その文はまったく同じように解釈されます。これら3つのケースでは、それぞれ正反対の対処が必要です。

解決策: attemptReport

失敗したすべてのProxyレスポンスには、エラーの横にattemptReportオブジェクトが含まれます。これには、各試行で実際に発生した問題がカウントされています。

{
  "error": "Download maxTry limit reached",
  "attemptReport": {
    "total": 25,
    "noResponse": 0,
    "defense": 0,
    "contentRejected": 25,
    "statusRejected": 0,
    "other": 0,
    "vendors": [],
    "profilesTried": ["default"],
    "summary": "25 attempt(s): 25 returned HTTP 200 with no defense present and were rejected only by your validate.data - the page was fetched, your content rule did not match it"
  },
  "total": 34.812
}

error 文字列は意図的に変更されていないため、これにマッチングしているクライアントはそのまま動作し続けます。1行の回答が必要な場合は attemptReport.summary を、分岐処理を行いたい場合は各カウントを参照してください。

フィールド

フィールド カウント対象
total integer 試行回数
noResponse integer 出口ノードが応答しなかったため、サイトに到達できなかった回数
defense integer サイトは応答したが、その応答で Bot 検知ベンダーが検出された回数
contentRejected integer HTTP 200、Bot 検知なしだが、validate.data のみによって拒否された回数
statusRejected integer サイトは応答し、Bot 検知もなかったが、validate.status によって拒否された回数
other integer 応答があり、上記のいずれにも該当しない回数
vendors string[] タスク内のいずれかで検出されたすべての Bot 検知ベンダー
profilesTried string[] タスクが送信したブラウザプロファイル(初回使用順)。default は、リクエストが記述どおりにそのまま送信されたことを意味します。
summary string カウントから生成された1文。ログ記録やユーザーへの表示に適しています。

結果の読み取り方

contentRejected が多い場合

ページは届いています。設定した validate.data ルールが一致しませんでした。

これはユーザー側で修正可能なケースであり、他のすべてのシグナルが覆い隠してしまうものです。すべてのメトリクスでリクエストが失敗に見えていても、FourA は終始実際のコンテンツを返しています。validate を一切設定せずに POST /api/single/ 経由で一度ページを取得し、実際に返される内容を確認した上で、それに対してルールを書き直してください。

よくある原因は、構造が異なるページ群に対して単一のルールを適用していることです。記事ページには存在し動画ページには存在しないセレクタを使用している場合、動画ページに到達するたびに例外なく失敗し、コストが無駄にかかり続けます。

statusRejected が多い場合

サイトは応答しましたが、validate.status ルールにより拒否されました。ステータスが 401、403、429、または 503 の場合、サイトはページが存在しないのではなくクライアントを拒絶しています。以下を試してください:

  • 別のブラウザプロファイル(内部の request オブジェクトの browserosversion
  • コンテンツが地域制限されている場合は exitCountries
  • 拒絶の解除に JavaScript の実行が必要な場合は POST /api/browser/

defense が多い場合

応答で Bot 検知が認識されました。vendors にその名称が記録されます。FourA が現在解除できる対象とレポートのみを行う対象については、Anti-Bot Defenses を参照してください。そのベンダーがこのエンドポイントで解除対象となっていない場合は、呼び出しを POST /api/browser/ または POST /api/auto/ に移行してください。

noResponse が多い場合

出口ノードが一切応答しなかったため、ターゲットに関する情報は取得できませんでした。maxTries を増やし、timeout_ms を増やし、URL がパブリックインターネットから名前解決できるか確認してください。

other が多い場合

応答はあったものの、上記のいずれにも分類されませんでした。total_timetimeout_ms と照合してください。タイムアウト予算よりも応答が遅いターゲットはここに分類されます。

ブラウザプロファイルのローテーション

サイトがFourAから送信されたブラウザを拒否した場合、Proxyはそのブラウザの試行を停止し、public profile catalogueから別のファミリーを試します。これに追加の試行コストはかかりません。ローテーションはリトライ時に送信する内容を変更するだけであり、リトライ自体の発生可否を変更することはありません。

profilesTriedで動作状況を確認できます。エントリが1つの場合は、毎回指定どおりにリクエストが送信されたことを意味します。複数の場合は、ローテーションが実行されたもののサイトがそのすべてを拒否したことを意味し、一度もローテーションを行わなかった場合とは状況が異なります。

成功した Proxy responseにおいて、profileフィールドはローテーションによって要求外のブラウザが選択された場合にのみ表示されます:

{
  "status": 200,
  "data": "<!doctype html>...",
  "proxy": "A1B2C3",
  "profile": "...",
  "total": 4.108
}

この値は GET /api/profiles のカタログ ID です。存在しない場合、request は指定された内容のまま送信されたことを意味します。存在する場合、実際に成功したブラウザが指定したものと異なっていたことを意味するため、失敗したブラウザを再試行するのではなく、以降の呼び出しでその ID を profile として渡してください。ダッシュボードの Playground では、Carry を使用してこの処理を自動的に行います。

request に明示的な profilebrowseros、または version が設定されている場合、上書きされることはありません。クリアランスはそれを取得したシグネチャに紐づいているため、独自の User-Agent または Cookie header を含む request も上書きされません。

コードでの読み取り

import requests

API = "https://eu.api.foura.ai"
H = {"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"}

r = requests.post(f"{API}/api/proxy/", headers=H, json={
    "maxTries": 5,
    "request": {
        "method": "GET",
        "url": "https://example.com/product/42",
        "validate": {"data": {"accept": ["Add to cart"]}},
    },
}).json()

if "error" in r:
    rep = r.get("attemptReport", {})
    print(rep.get("summary", r["error"]))

    if rep.get("contentRejected", 0) > rep.get("total", 0) / 2:
        # The pages arrived. The validate rule is what threw them away.
        raise SystemExit("validate.data did not match the real page")
    if rep.get("defense", 0):
        print("bot check met:", ", ".join(rep.get("vendors", [])))

関連ドキュメント

最終更新日: 2026年8月31日