Tutorials

Node.js CAPTCHA-Lösung mit Wiederholungsversuch und Fehlerbehandlung

Die Lösung des Produktions-CAPTCHA schlägt fehl. APIs laufen ab, Token laufen ab, Guthaben gehen zur Neige. In diesem Leitfaden werden Fehlerklassifizierung, Wiederholungsstrategien, Leistungsschalter und Überwachung für Node.js + CaptchaAI behandelt.


Fehlerklassifizierung

const RETRIABLE_ERRORS = new Set([
  "ERROR_NO_SLOT_AVAILABLE",
  "CAPCHA_NOT_READY",
]);

const FATAL_ERRORS = new Set([
  "ERROR_WRONG_USER_KEY",
  "ERROR_KEY_DOES_NOT_EXIST",
  "ERROR_ZERO_BALANCE",
  "ERROR_CAPTCHA_UNSOLVABLE",
  "ERROR_BAD_DUPLICATES",
  "ERROR_BAD_PARAMETERS",
  "ERROR_WRONG_CAPTCHA_ID",
]);

class CaptchaError extends Error {
  constructor(code, message) {
    super(message || code);
    this.name = "CaptchaError";
    this.code = code;
  }
}

class RetriableError extends CaptchaError {
  constructor(code) {
    super(code, `Retriable: ${code}`);
    this.name = "RetriableError";
  }
}

class FatalError extends CaptchaError {
  constructor(code) {
    super(code, `Fatal: ${code}`);
    this.name = "FatalError";
  }
}

function classifyError(code) {
  if (FATAL_ERRORS.has(code)) throw new FatalError(code);
  throw new RetriableError(code);
}

Exponentieller Backoff

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function withRetry(fn, options = {}) {
  const {
    maxRetries = 3,
    baseDelay = 2000,
    maxDelay = 30000,
    jitter = true,
  } = options;

  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (error instanceof FatalError) throw error;

      lastError = error;

      if (attempt < maxRetries) {
        let delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
        if (jitter) delay *= 0.5 + Math.random();
        console.log(
          `Retry ${attempt + 1}/${maxRetries} in ${(delay / 1000).toFixed(1)}s: ${error.message}`
        );
        await sleep(delay);
      }
    }
  }

  throw lastError;
}

Robuster Löser

const API_KEY = "YOUR_API_KEY";

class RobustSolver {
  #apiKey;
  #maxRetries;
  #pollInterval;
  #maxPollTime;

  constructor(apiKey, options = {}) {
    this.#apiKey = apiKey;
    this.#maxRetries = options.maxRetries ?? 3;
    this.#pollInterval = options.pollInterval ?? 5000;
    this.#maxPollTime = options.maxPollTime ?? 150000;
  }

  async solve(method, params) {
    return withRetry(
      () => this.#doSolve(method, params),
      { maxRetries: this.#maxRetries }
    );
  }

  async #doSolve(method, params) {
    const taskId = await this.#submit(method, params);
    return await this.#poll(taskId);
  }

  async #submit(method, params) {
    for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
      try {
        const resp = await fetch("https://ocr.captchaai.com/in.php", {
          method: "POST",
          body: new URLSearchParams({
            key: this.#apiKey,
            method,
            json: "1",
            ...params,
          }),
          signal: AbortSignal.timeout(30000),
        });

        if (!resp.ok) {
          throw new RetriableError(`HTTP_${resp.status}`);
        }

        const data = await resp.json();

        if (data.status === 1) return data.request;

        if (data.request === "ERROR_NO_SLOT_AVAILABLE") {
          if (attempt < this.#maxRetries) {
            await sleep(3000 * (attempt + 1));
            continue;
          }
        }

        classifyError(data.request);
      } catch (error) {
        if (error instanceof FatalError) throw error;
        if (error.name === "TimeoutError" || error.name === "AbortError") {
          if (attempt < this.#maxRetries) {
            await sleep(2000 * (attempt + 1));
            continue;
          }
        }
        throw error;
      }
    }
    throw new RetriableError("MAX_SUBMIT_RETRIES");
  }

  async #poll(taskId) {
    const start = Date.now();

    while (Date.now() - start < this.#maxPollTime) {
      await sleep(this.#pollInterval);

      try {
        const resp = await fetch(
          `https://ocr.captchaai.com/res.php?${new URLSearchParams({
            key: this.#apiKey,
            action: "get",
            id: taskId,
            json: "1",
          })}`,
          { signal: AbortSignal.timeout(30000) }
        );

        const data = await resp.json();

        if (data.status === 1) return data.request;
        if (data.request === "CAPCHA_NOT_READY") continue;
        if (FATAL_ERRORS.has(data.request)) throw new FatalError(data.request);
      } catch (error) {
        if (error instanceof FatalError) throw error;
        // Network errors during poll — keep trying
        continue;
      }
    }

    throw new CaptchaError("TIMEOUT", `Timed out after ${this.#maxPollTime}ms`);
  }
}

Leistungsschalter

class CircuitBreaker {
  #state = "closed"; // closed | open | half-open
  #failures = 0;
  #lastFailure = 0;
  #threshold;
  #resetTimeout;

  constructor(threshold = 5, resetTimeout = 60000) {
    this.#threshold = threshold;
    this.#resetTimeout = resetTimeout;
  }

  get state() {
    return this.#state;
  }

  canExecute() {
    if (this.#state === "closed") return true;
    if (this.#state === "open") {
      if (Date.now() - this.#lastFailure > this.#resetTimeout) {
        this.#state = "half-open";
        return true;
      }
      return false;
    }
    return true; // half-open: allow test request
  }

  recordSuccess() {
    this.#failures = 0;
    this.#state = "closed";
  }

  recordFailure() {
    this.#failures++;
    this.#lastFailure = Date.now();
    if (this.#failures >= this.#threshold) {
      this.#state = "open";
      console.log(`Circuit OPEN — pausing for ${this.#resetTimeout / 1000}s`);
    }
  }
}

class ProtectedSolver {
  #solver;
  #breaker;

  constructor(apiKey) {
    this.#solver = new RobustSolver(apiKey);
    this.#breaker = new CircuitBreaker(5, 60000);
  }

  async solve(method, params) {
    if (!this.#breaker.canExecute()) {
      throw new CaptchaError(
        "CIRCUIT_OPEN",
        "API appears down — circuit breaker is open"
      );
    }

    try {
      const result = await this.#solver.solve(method, params);
      this.#breaker.recordSuccess();
      return result;
    } catch (error) {
      if (error instanceof FatalError) throw error;
      this.#breaker.recordFailure();
      throw error;
    }
  }

  get circuitState() {
    return this.#breaker.state;
  }
}

Token-Ablaufhandler

class TokenCache {
  #cache = new Map();
  #defaultTTL;

  constructor(defaultTTL = 110000) {
    // reCAPTCHA: ~2 min, Turnstile: ~5 min
    this.#defaultTTL = defaultTTL;
  }

  get(key) {
    const entry = this.#cache.get(key);
    if (!entry) return null;
    if (Date.now() - entry.timestamp > this.#defaultTTL) {
      this.#cache.delete(key);
      return null;
    }
    return entry.token;
  }

  set(key, token) {
    this.#cache.set(key, { token, timestamp: Date.now() });
  }

  invalidate(key) {
    this.#cache.delete(key);
  }
}

class CachedSolver {
  #solver;
  #cache;

  constructor(apiKey) {
    this.#solver = new ProtectedSolver(apiKey);
    this.#cache = new TokenCache(110000);
  }

  async getToken(cacheKey, method, params) {
    const cached = this.#cache.get(cacheKey);
    if (cached) return cached;

    const token = await this.#solver.solve(method, params);
    this.#cache.set(cacheKey, token);
    return token;
  }

  async solveWithRetryOnReject(method, params, submitFn, maxAttempts = 2) {
    for (let i = 0; i < maxAttempts; i++) {
      const token = await this.#solver.solve(method, params);
      const accepted = await submitFn(token);
      if (accepted) return token;
      console.log(`Token rejected (attempt ${i + 1}), re-solving...`);
    }
    throw new CaptchaError("TOKEN_REJECTED", "Token rejected after max attempts");
  }
}

Protokollierung und Metriken

class SolverMetrics {
  #startTime = Date.now();
  #solveTimes = [];
  #counts = { submitted: 0, solved: 0, failed: 0, retries: 0 };

  recordSubmit() { this.#counts.submitted++; }
  recordSolved(duration) { this.#counts.solved++; this.#solveTimes.push(duration); }
  recordFailed() { this.#counts.failed++; }
  recordRetry() { this.#counts.retries++; }

  report() {
    const elapsed = (Date.now() - this.#startTime) / 1000;
    const total = this.#counts.solved + this.#counts.failed;
    const avgTime = this.#solveTimes.length > 0
      ? this.#solveTimes.reduce((a, b) => a + b, 0) / this.#solveTimes.length / 1000
      : 0;

    return {
      elapsed: `${elapsed.toFixed(0)}s`,
      submitted: this.#counts.submitted,
      solved: this.#counts.solved,
      failed: this.#counts.failed,
      retries: this.#counts.retries,
      avgSolveTime: `${avgTime.toFixed(1)}s`,
      successRate: total > 0 ? `${((this.#counts.solved / total) * 100).toFixed(1)}%` : "N/A",
      throughput: `${(this.#counts.solved / (elapsed / 60)).toFixed(1)}/min`,
    };
  }
}

class InstrumentedSolver {
  #solver;
  #metrics;

  constructor(apiKey) {
    this.#solver = new ProtectedSolver(apiKey);
    this.#metrics = new SolverMetrics();
  }

  async solve(method, params) {
    this.#metrics.recordSubmit();
    const start = Date.now();

    try {
      const token = await this.#solver.solve(method, params);
      this.#metrics.recordSolved(Date.now() - start);
      return token;
    } catch (error) {
      this.#metrics.recordFailed();
      throw error;
    }
  }

  report() {
    return this.#metrics.report();
  }
}

Vollständiges Produktionsmuster

// Combine everything
const solver = new InstrumentedSolver("YOUR_API_KEY");

async function main() {
  const tasks = Array.from({ length: 10 }, (_, i) => ({
    method: "userrecaptcha",
    params: { googlekey: `KEY_${i}`, pageurl: `https://example.com/${i}` },
  }));

  const results = await Promise.allSettled(
    tasks.map((task) => solver.solve(task.method, task.params))
  );

  const solved = results.filter((r) => r.status === "fulfilled");
  const failed = results.filter((r) => r.status === "rejected");

  console.log(`Solved: ${solved.length}, Failed: ${failed.length}`);
  console.log("Metrics:", solver.report());

  for (const fail of failed) {
    console.log(`  Error: ${fail.reason.message}`);
  }
}

main();

Fehlerbehebung

Symptom Ursache Beheben
Alle Wiederholungsversuche schlagen sofort fehl Schwerwiegender Fehler beim erneuten Versuch Fehlerklassifizierung prüfen
Leistungsschalter bleibt geöffnet API ausgefallen oder falscher Schlüssel Überprüfen Sie den API-Status und den API-Schlüssel
Das Token ist beim Absenden abgelaufen Lösungszeit + Verzögerung zu lang Vor dem Navigieren vorab lösen
AbortError beim Abruf Timeout zu kurz Erhöhen Sie AbortSignal.timeout
UnhandledPromiseRejection Fehlender Haken bei Async Gehen Sie stets mit Ablehnungen um

Häufig gestellte Fragen

Soll ich ERROR_CAPTCHA_UNSOLVABLE erneut versuchen?

Nein. Dies ist ein schwerwiegender Fehler – das CAPTCHA konnte nicht gelöst werden. Ein erneuter Versuch verschwendet Zeit und Geld.

Was ist die richtige Anzahl an Wiederholungsversuchen?

3 Wiederholungsversuche beim Absenden, 30 Umfragen für Ergebnisse. Wenn drei erneute Übermittlungsversuche fehlschlagen, stimmt etwas grundsätzlich nicht.

Woher weiß ich, ob es sich um einen Netzwerkfehler oder einen API-Fehler handelt?

Netzwerkfehler lösen TypeError (Abruf fehlgeschlagen) oder AbortError (Zeitüberschreitung) aus. API-Fehler geben JSON mit einem Fehlercode zurück. Gehen Sie anders mit ihnen um.


Zusammenfassung

Robuste Node.js CAPTCHA-Lösung mitCaptchaAI: Fehler als wiederholbar oder schwerwiegend klassifizieren, exponentielles Backoff mit Jitter verwenden, mit Schutzschaltern schützen, Token zur Wiederverwendung zwischenspeichern und mit Metriken für die Produktionstransparenz ausstatten.

Verwandte Artikel


Verwandte Leitfäden

Diskussionen (0)

Noch keine Kommentare.