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

問題

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

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

その文は、すべてのexitがブロックされた場合でも、すべてのexitが停止していた場合でも、あるいはFourAがほぼすべての試行で実際のページを取得したものの設定したvalidateルールによって破棄された場合でも、まったく同じように表示されます。これら3つの状況にはそれぞれ異なる対処が必要です。

解決策: attemptReport

試行を実行した後に失敗したすべてのProxy呼び出しには、"error": "Timeout"で終了したものを含め、エラーと共に追加のattemptReportオブジェクトが含まれます。名前解決できないホスト名は試行前に失敗し、レポートなしでstatus: 0が返されます。このオブジェクトは、各試行で実際に発生した事象をカウントします。

{
  "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/経由でページを1回取得し、実際のレスポンス内容を確認した上でルールを書き直してください。

よくある原因は、構造が異なるページ群に対して単一のルールを適用していることです。記事ページには存在するが動画ページには存在しないセレクターを指定していると、動画ページにアクセスするたびに毎回失敗し、無駄なコストが発生し続けます。

statusRejectedが高い場合

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

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

defenseが高い場合

応答内でBot対策が検出されました。具体的な種別はvendorsに示されます。FourAが現在自動解除できる対象とレポートのみ行う対象については、サイトチェックを参照してください。該当ベンダーがこのendpointで解除対象外の場合は、呼び出しをPOST /api/browser/またはPOST /api/auto/に切り替えてください。

noResponseが高い場合

イグジットから全く応答がなかったため、ターゲットに関する情報は得られませんでした。maxTriesの引き上げ、timeout_msの引き上げを行い、パブリックインターネットからURLが名前解決できるか確認してください。

otherが高い場合

サイトはステータスを返送し、Bot対策も検出されませんでしたが、validate.headersルールなどの別の理由で試行が失敗しました。設定されたtimeout_msより応答が遅いターゲットはnoResponseとしてカウントされます。

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

FourAが送信したブラウザをサイトが拒否した場合、Proxyはそのブラウザの再送を中止し、public profile catalogueから別のファミリを試行します。試行回数は追加で消費されません。ローテーションによってリトライ時に送信される内容が変更されるだけであり、リトライの実行有無自体が変わることはありません。

また、Proxyはサイトごとに、デフォルトが拒否された後に最後に成功したファミリを記憶します。そのサイトへの以降のリクエストでは初回の試行からそのファミリを使用でき、サイトがそのファミリも拒否し始めた場合は、FourAは次のファミリへと移行します。モバイルブラウザが記憶されるのは、JSONを返すサイトのみです。

この動作状況はprofilesTriedで確認できます。エントリが1つの場合は、すべての試行で同一のブラウザが送信されたことを意味します。defaultは記述通りのリクエストであり、カタログIDはFourAがそのサイト用にすでに学習していたファミリです。複数のエントリがある場合は、ローテーションが実行され、サイトがそれらをすべて拒否したことを示しており、まったくローテーションが行われなかった状況とは異なります。

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

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

この値は GET /api/profiles のカタログ ID です。値がない場合は、リクエストが指定通りにそのまま送信されたことを意味します。値がある場合は、動作したブラウザが指定したものと異なっていたことを意味するため、失敗したブラウザを再試行するのではなく、後続の呼び出しでその ID を profile として渡してください。ダッシュボードの Playground では、Carry を使用してこれが自動的に行われます。

リクエストで明示的に指定された profile、browser、os、または version が上書きされることはありません。また、クリアランスはそれを取得したシグネチャにバインドされているため、独自の User-Agent または Cookie ヘッダーを含むリクエストも上書きされません。

コードでの読み取り

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年9月30日