API Tutorials

Modèle de disjoncteur pour les appels d'API CAPTCHA

Un disjoncteur (circuit breaker) coupe automatiquement les appels vers l'API de résolution CAPTCHA dès qu'elle enchaîne les erreurs, puis rétablit le trafic tout seul une fois qu'elle répond de nouveau. Résultat : vous cessez de payer des requêtes vouées à l'échec pendant un incident, et une panne de l'API ne se propage plus à tout votre pipeline. Ce guide montre comment le brancher autour de l'API CaptchaAI, en Python puis en Node.js.

L'intérêt se voit surtout sous charge. Imaginez un pipeline de scraping hébergé sur des workers Scaleway ou OVHcloud qui envoie quelques centaines de résolutions par minute. Si l'API se met à renvoyer ERROR_NO_SLOT_AVAILABLE en rafale, un pipeline naïf continue de marteler l'endpoint et accumule les timeouts. Avec un disjoncteur, le circuit s'ouvre après quelques échecs, les tâches suivantes échouent instantanément (sans attente réseau) et le système teste la reprise de lui-même.


Les trois états d'un disjoncteur

Un disjoncteur passe par trois états :

  1. Fermé (closed) – Fonctionnement normal. Les requêtes passent vers l'API et chaque échec est compté.
  2. Ouvert (open) – Le seuil d'échecs est franchi. Toutes les requêtes sont rejetées immédiatement, sans appeler l'API.
  3. Semi-ouvert (half-open) – Après un délai de refroidissement, une seule requête de test est laissée passer. Si elle réussit, le circuit se referme ; si elle échoue, il se rouvre.

C'est cette phase semi-ouverte qui rend le mécanisme automatique : personne n'a besoin de rouvrir manuellement le trafic après un incident.


Disjoncteur en Python pour l'API CaptchaAI

Voici une implémentation autonome, protégée par un verrou (threading.Lock) pour rester correcte quand plusieurs threads partagent le même disjoncteur. La méthode call() enveloppe n'importe quelle fonction — ici solve_captcha, qui soumet un reCAPTCHA v2 puis interroge le résultat :

import time
import threading
import requests

SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
API_KEY = "YOUR_API_KEY"


class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=60):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failure_count = 0
        self.last_failure_time = 0
        self.state = "closed"  # closed, open, half-open
        self._lock = threading.Lock()

    def call(self, func, *args, **kwargs):
        with self._lock:
            if self.state == "open":
                if time.time() - self.last_failure_time > self.recovery_timeout:
                    self.state = "half-open"
                    print("[circuit] State: half-open — testing one request")
                else:
                    remaining = self.recovery_timeout - (
                        time.time() - self.last_failure_time
                    )
                    raise CircuitOpenError(
                        f"Circuit open — retry in {remaining:.0f}s"
                    )

        try:
            result = func(*args, **kwargs)
            with self._lock:
                self.failure_count = 0
                if self.state == "half-open":
                    print("[circuit] State: closed — API recovered")
                self.state = "closed"
            return result
        except Exception as e:
            with self._lock:
                self.failure_count += 1
                self.last_failure_time = time.time()
                if self.failure_count >= self.failure_threshold:
                    self.state = "open"
                    print(
                        f"[circuit] State: open — "
                        f"{self.failure_count} failures"
                    )
            raise


class CircuitOpenError(Exception):
    pass


def solve_captcha(sitekey, page_url):
    resp = requests.post(SUBMIT_URL, data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": page_url,
        "json": "1",
    }, timeout=15)
    data = resp.json()
    if data["status"] != 1:
        raise Exception(f"Submit error: {data['request']}")

    task_id = data["request"]
    for _ in range(24):
        time.sleep(5)
        poll = requests.get(RESULT_URL, params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": "1",
        }, timeout=15).json()
        if poll["status"] == 1:
            return poll["request"]
        if poll["request"] != "CAPCHA_NOT_READY":
            raise Exception(f"Poll error: {poll['request']}")
    raise TimeoutError(f"Task {task_id} timed out")


# Usage
breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=30)

for i in range(10):
    try:
        token = breaker.call(
            solve_captcha, "6Le-SITEKEY", "https://example.com"
        )
        print(f"[task-{i}] Solved: {token[:40]}...")
    except CircuitOpenError as e:
        print(f"[task-{i}] Skipped: {e}")
    except Exception as e:
        print(f"[task-{i}] Failed: {e}")

Résultat attendu :

[task-0] Solved: 03AGdBq26ZfPxL...
[task-1] Solved: 03AGdBq27AbCdE...
[task-2] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[task-3] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[task-4] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[circuit] State: open — 3 failures
[task-5] Skipped: Circuit open — retry in 28s
[task-6] Skipped: Circuit open — retry in 25s
...
[circuit] State: half-open — testing one request
[task-8] Solved: 03AGdBq28FgHiJ...
[circuit] State: closed — API recovered

Le circuit s'ouvre après trois ERROR_NO_SLOT_AVAILABLE consécutifs, rejette les tâches suivantes sans le moindre appel réseau, puis laisse passer une requête de test en semi-ouvert avant de se refermer.


Disjoncteur en JavaScript (Node.js)

La même logique côté Node.js, pour un pipeline piloté par événements. Ici recoveryTimeout s'exprime en millisecondes ; ajustez-le à votre trafic :

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.recoveryTimeout = options.recoveryTimeout || 60000;
    this.failureCount = 0;
    this.lastFailureTime = 0;
    this.state = 'closed';
  }

  async call(fn, ...args) {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
        this.state = 'half-open';
        console.log('[circuit] State: half-open');
      } else {
        const remaining = this.recoveryTimeout - (Date.now() - this.lastFailureTime);
        throw new Error(`Circuit open — retry in ${Math.ceil(remaining / 1000)}s`);
      }
    }

    try {
      const result = await fn(...args);
      this.failureCount = 0;
      if (this.state === 'half-open') {
        console.log('[circuit] State: closed — recovered');
      }
      this.state = 'closed';
      return result;
    } catch (error) {
      this.failureCount++;
      this.lastFailureTime = Date.now();
      if (this.failureCount >= this.failureThreshold) {
        this.state = 'open';
        console.log(`[circuit] State: open — ${this.failureCount} failures`);
      }
      throw error;
    }
  }
}

// Usage
const axios = require('axios');

const API_KEY = 'YOUR_API_KEY';
const breaker = new CircuitBreaker({ failureThreshold: 3, recoveryTimeout: 30000 });

async function solveCaptcha(sitekey, pageurl) {
  const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
    params: { key: API_KEY, method: 'userrecaptcha', googlekey: sitekey, pageurl, json: 1 }
  });

  if (submit.data.status !== 1) throw new Error(submit.data.request);
  const taskId = submit.data.request;

  for (let i = 0; i < 24; i++) {
    await new Promise(r => setTimeout(r, 5000));
    const poll = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: API_KEY, action: 'get', id: taskId, json: 1 }
    });
    if (poll.data.status === 1) return poll.data.request;
    if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
  }
  throw new Error('Timeout');
}

(async () => {
  for (let i = 0; i < 10; i++) {
    try {
      const token = await breaker.call(solveCaptcha, '6Le-SITEKEY', 'https://example.com');
      console.log(`[task-${i}] Solved: ${token.substring(0, 40)}...`);
    } catch (err) {
      console.log(`[task-${i}] ${err.message}`);
    }
  }
})();

Régler les seuils selon votre trafic

Deux paramètres pilotent tout le comportement : le nombre d'échecs qui ouvre le circuit et le délai avant de tester la reprise. Partez de ces repères, puis affinez avec vos propres mesures :

Paramètre Faible trafic (< 10/min) Trafic élevé (> 100/min)
failure_threshold 3 10
recovery_timeout 30 s 60 s

Fixez failure_threshold assez haut pour absorber les erreurs passagères — un timeout isolé ne doit pas ouvrir le circuit — mais assez bas pour cesser rapidement de solliciter une API en panne.


Combiner disjoncteur et logique de retry

Placez la logique de nouvelle tentative à l'intérieur du disjoncteur, jamais l'inverse. Ainsi, le disjoncteur ne compte que les échecs définitifs, une fois les tentatives épuisées :

def solve_with_retry(sitekey, page_url, max_retries=2):
    for attempt in range(max_retries + 1):
        try:
            return solve_captcha(sitekey, page_url)
        except Exception:
            if attempt == max_retries:
                raise
            time.sleep(2 ** attempt)

# Circuit breaker wraps the retry function
token = breaker.call(solve_with_retry, "6Le-SITEKEY", "https://example.com")

Le backoff exponentiel (2 ** attempt) espace les tentatives, et le circuit ne s'ouvre que si l'API reste indisponible une fois toutes les tentatives épuisées.


Dépannage

Problème Cause probable Correctif
Le circuit s'ouvre trop tôt failure_threshold trop bas Augmentez failure_threshold pour tolérer les erreurs passagères
Le circuit ne se referme jamais recovery_timeout trop long Ramenez-le à 30–60 s
Incohérence d'état en multithread Aucun verrou sur l'état partagé Protégez l'état avec threading.Lock (Python) ou des opérations atomiques
Tout le trafic bloqué pendant une panne partielle Un seul disjoncteur pour tous les endpoints Séparez les disjoncteurs des endpoints de soumission et d'interrogation

FAQ

Un disjoncteur ralentit-il le pipeline en fonctionnement normal ?

Non. Tant que le circuit est fermé, il se contente d'incrémenter un compteur d'échecs : le coût est négligeable. Le gain apparaît pendant un incident, quand les tâches sont rejetées en quelques microsecondes au lieu d'attendre un timeout réseau de 15 s.

Faut-il un disjoncteur par worker ou un seul partagé ?

Pour un pipeline multi-worker, préférez un disjoncteur par processus (ou par machine). Un état partagé via Redis est possible, mais il ajoute une dépendance réseau qui peut elle-même tomber — l'inverse de l'objectif recherché.

Le disjoncteur consomme-t-il des threads de mon plan CaptchaAI ?

Non. Les threads que vous payez (à partir de 5 avec le plan BASIC, $15/mois) correspondent aux résolutions CAPTCHA en cours côté API. Le disjoncteur vit dans votre code : lorsque le circuit est ouvert, il n'envoie aucune requête et vous fait donc plutôt économiser des threads pendant un incident.

Que deviennent les tâches CAPTCHA rejetées quand le circuit est ouvert ?

À vous de décider : remettez-les en file d'attente pour un nouvel essai différé, ou basculez sur la dégradation gracieuse côté interface. L'essentiel est de ne jamais les perdre silencieusement.


Des workflows CAPTCHA résilients avec CaptchaAI

Obtenez votre clé API sur captchaai.com, puis branchez le disjoncteur ci-dessus sur vos appels de résolution.


Guides associés

Les commentaires sont désactivés pour cet article.