Proxy Request 재시도 횟수가 초과된 이유

문제

POST /api/proxy/ 호출 시 데이터 없이 에러가 반환됩니다. 메시지는 짧고 항상 동일한 형태입니다:

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

모든 출구가 차단되었든, 모든 출구가 작동하지 않았든, 혹은 FourA가 거의 매 시도마다 실제 페이지를 가져왔으나 사용자의 validate 규칙에 의해 폐기되었든 이 문장은 동일하게 표시됩니다. 이 세 가지 상황에는 각각 정반대의 해결책이 필요합니다.

해결 방법: attemptReport

실패한 모든 Proxy response에는 오류와 함께 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 문자열은 기존 클라이언트와의 호환성을 유지하기 위해 의도적으로 변경되지 않습니다. 한 줄 요약은 attemptReport.summary에서 확인하고, 조건부 분기 처리가 필요한 경우 카운트 값을 사용하세요.

필드

필드 타입 집계 대상
total integer 시도 횟수
noResponse integer exit 노드가 응답하지 않아 사이트에 도달하지 못함
defense integer 사이트가 응답했으나 봇 차단 솔루션이 감지됨
contentRejected integer HTTP 200 수신, 봇 감지 없음, validate.data 조건에 의해서만 거부됨
statusRejected integer 사이트가 응답함, 봇 감지 없음, validate.status 조건에 의해 거부됨
other integer 응답을 받았으나 위 항목 중 어디에도 해당하지 않음
vendors string[] 태스크 내에서 감지된 모든 봇 차단 솔루션 목록
profilesTried string[] 태스크가 전송한 브라우저 프로필 목록 (최초 사용 순서). default는 요청이 작성된 그대로 전송되었음을 의미합니다.
summary string 카운트 기반으로 생성된 단일 문장. 로그 기록이나 사용자 표시에 안전합니다.

분석 방법

contentRejected 수치가 높은 경우

페이지는 정상 수신되었으나 validate.data 규칙과 일치하지 않았습니다.

이 문제는 직접 수정할 수 있으며, 다른 지표에서는 드러나지 않는 문제입니다. 모든 지표상으로는 실패로 보이지만, FourA는 정상적인 콘텐츠를 계속 전달하고 있었습니다. validate 없이 POST /api/single/를 통해 페이지를 한 번 직접 호출하여 실제 응답 내용을 확인한 후 규칙을 다시 작성하세요.

서로 다른 구조를 가진 페이지 세트에 동일한 규칙을 적용하는 것이 흔한 원인입니다. 예를 들어 글 페이지에는 존재하지만 동영상 페이지에는 없는 선택자를 사용하면, 동영상 페이지에 도달할 때마다 매번 실패하며 비용이 그대로 발생합니다.

statusRejected 수치가 높은 경우

사이트가 응답했으나 validate.status 규칙에 의해 거부되었습니다. 상태 코드가 401, 403, 429, 503인 경우 사이트가 페이지 부재를 알리는 것이 아니라 클라이언트를 차단하는 것입니다. 다음 조치를 시도하세요.

  • 다른 브라우저 프로필 적용 (내부 request 객체의 browser, os, version)
  • 콘텐츠가 특정 지역으로 제한된 경우 exitCountries 사용
  • 차단 해제에 JavaScript 실행이 필요한 경우 POST /api/browser/ 사용

defense 수치가 높은 경우

응답에서 봇 차단 시스템이 감지되었으며, vendors에 해당 솔루션명이 명시됩니다. FourA가 현재 우회 가능한 솔루션과 리포팅만 지원하는 솔루션은 Anti-Bot Defenses를 참고하세요. 해당 엔드포인트에서 우회되지 않는 솔루션인 경우 호출 대상을 POST /api/browser/ 또는 POST /api/auto/로 변경하세요.

noResponse 수치가 높은 경우

exit 노드가 전혀 응답하지 않아 대상 사이트에 대한 정보를 얻지 못했습니다. maxTriestimeout_ms 값을 늘리고, 공개 인터넷에서 해당 URL의 DNS 조회가 정상적으로 수행되는지 확인하세요.

other 수치가 높은 경우

응답을 받았으나 위의 어느 범주에도 속하지 않았습니다. total_time를 설정된 timeout_ms와 비교 확인하세요. 대상 사이트의 응답 시간이 허용 예산보다 느리면 여기에 분류됩니다.

브라우저 프로필 순환

사이트가 FourA에서 보낸 브라우저를 거부하면 Proxy는 해당 브라우저를 계속 고집하지 않고 공개 프로필 카탈로그에서 다른 제품군을 시도합니다. 추가 시도 비용은 들지 않습니다. 로테이션은 재시도 시 전송되는 내용을 변경할 뿐 재시도 발생 여부 자체를 바꾸지는 않습니다.

profilesTried 항목을 통해 이 과정을 확인할 수 있습니다. 항목이 하나라면 request가 매번 작성된 그대로 전송되었음을 의미합니다. 항목이 여러 개라면 로테이션이 실행되었으나 사이트가 각각을 거부했음을 의미하며, 이는 아예 로테이션을 수행하지 않은 것과는 다른 상황입니다.

성공한 Proxy response에서 profile 필드는 로테이션을 통해 요청하지 않은 브라우저가 선택되었을 때만 나타납니다.

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

값은 GET /api/profiles의 카탈로그 ID입니다. 이 값이 없으면 request가 작성된 그대로 전송되었음을 의미합니다. 값이 있으면 성공한 브라우저가 직접 입력한 브라우저와 달랐음을 의미하므로, 실패한 브라우저를 재시도하는 대신 후속 호출 시 해당 ID를 profile로 다시 전달하십시오. 대시보드 PlaygroundCarry 기능을 통해 이 작업을 자동으로 처리합니다.

request에 명시된 profile, browser, os 또는 version은 절대 재정의되지 않습니다. 고유한 User-Agent 또는 Cookie header가 포함된 request 역시 재정의되지 않으며, 이는 clearance가 이를 획득한 서명에 바인딩되어 있기 때문입니다.

코드에서 읽기

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일