La résolution du CAPTCHA en production échoue. Les API expirent, les jetons expirent, les soldes s'épuisent. Ce guide couvre la classification des erreurs, les stratégies de nouvelle tentative, les disjoncteurs et la surveillance pour Node.js + CaptchaAI.
Classement des erreurs
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);
}
Retard exponentiel
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;
}
Solveur robuste
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`);
}
}
Disjoncteur
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;
}
}
Gestionnaire d'expiration de jeton
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");
}
}
Journalisation et métriques
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();
}
}
Modèle de production complet
// 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();
Dépannage
| Symptôme | Parce que | Corriger |
|---|---|---|
| Toutes les tentatives échouent immédiatement | Erreur fatale lors de la nouvelle tentative | Vérifier la classification des erreurs |
| Le disjoncteur reste ouvert | API en panne ou mauvaise clé | Vérifier l'état et la clé de l'API |
| Le jeton a expiré lors de la soumission | Résoudre le temps + le délai trop long | Pré-résoudre avant de naviguer |
AbortError lors de la récupération |
Délai d'attente trop court | Augmenter AbortSignal.timeout |
| UnhandledPromiseRejection | Capture manquante en asynchrone | Gérez toujours les refus |
Questions fréquemment posées
Dois-je réessayer ERROR_CAPTCHA_UNSOLVABLE ?
Non. Il s'agit d'une erreur fatale : le CAPTCHA n'a pas pu être résolu. Réessayer fait perdre du temps et de l'argent.
Quel est le nombre de tentatives correct ?
3 tentatives lors de la soumission, 30 sondages pour les résultats. Si 3 tentatives de soumission échouent, quelque chose ne va pas du tout.
Comment savoir s'il s'agit d'une erreur réseau ou d'une erreur API ?
Les erreurs réseau renvoient TypeError (échec de la récupération) ou AbortError (délai d'attente). Les erreurs d'API renvoient JSON avec un code d'erreur. Manipulez-les différemment.
Résumé
Résolution robuste de CAPTCHA Node.js avecCaptchaAI: classez les erreurs comme récupérables ou fatales, utilisez l'intervalle exponentiel avec gigue, protégez avec des disjoncteurs, mettez en cache des jetons pour la réutilisation et instrumentez avec des métriques pour la visibilité de la production.
Articles connexes
Étapes suivantes
- Démarrage rapide CaptchaAI : votre première résolution de CAPTCHA en 5 minutes
- Comment résoudre reCAPTCHA v2 via l'API : guide pas à pas
- Comment résoudre Cloudflare Turnstile via l'API
- Comment résoudre GeeTest v3 à l'aide de l'API