Tutorials

Erros de callback da CaptchaAI: padrões de retentativa e fila de mensagens mortas

Nunca confie só no callback — essa é a regra de quem já perdeu solução em produção. O pingback da CaptchaAI elimina o polling manual na maior parte do tempo, mas abre um novo ponto de falha.

Trate o handler /callback como qualquer endpoint exposto: ele vai cair, dar timeout ou processar a mesma requisição duas vezes. Este tutorial cobre três padrões combináveis, com código pronto em Python e Node.js.

Os quatro jeitos de um callback dar errado

  • Servidor fora do ar — conexão recusada; solução não entregue.
  • Servidor devolve 5xx — resposta de erro; reentrega não confirmada (depende do lado deles).
  • Timeout de rede — conexão trava no meio da entrega; solução pode se perder.
  • Handler quebra no meio — requisição aceita, nada gravado; solução some silenciosamente.

Resultado prático: o worker que enviou a tarefa nunca recebe a resposta. A saída é redundância — nunca dependa de um único caminho de entrega.

Padrão 1: callback com polling de fallback

A abordagem mais confiável: aceite o callback quando chegar, mas mantenha um poller em segundo plano para qualquer tarefa sem resposta dentro do tempo limite.

Python

import os
import time
import threading
import requests
from flask import Flask, request

app = Flask(__name__)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]

# Track task state
pending_tasks = {}  # task_id -> {"submitted_at": timestamp, "status": "pending"}
results = {}
lock = threading.Lock()


def submit_captcha(sitekey, pageurl, callback_url):
    """Submit with callback, but track for fallback polling."""
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "pingback": callback_url,
        "json": 1
    })
    data = resp.json()

    if data.get("status") == 1:
        task_id = data["request"]
        with lock:
            pending_tasks[task_id] = {
                "submitted_at": time.time(),
                "status": "pending"
            }
        return task_id
    return None


@app.route("/callback")
def captcha_callback():
    """Primary result delivery — CaptchaAI sends results here."""
    task_id = request.args.get("id")
    solution = request.args.get("code")

    with lock:
        results[task_id] = solution
        pending_tasks.pop(task_id, None)

    return "OK", 200


def fallback_poller():
    """Poll for any tasks that missed their callback."""
    while True:
        time.sleep(30)  # Check every 30 seconds

        with lock:
            stale_tasks = [
                tid for tid, info in pending_tasks.items()
                if time.time() - info["submitted_at"] > 120  # 2 min callback timeout
                and info["status"] == "pending"
            ]

        for task_id in stale_tasks:
            resp = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY,
                "action": "get",
                "id": task_id,
                "json": 1
            })
            data = resp.json()

            if data.get("status") == 1:
                with lock:
                    results[task_id] = data["request"]
                    pending_tasks.pop(task_id, None)
                print(f"Fallback poll recovered: {task_id}")
            elif data.get("request") != "CAPCHA_NOT_READY":
                # Permanent error — remove from pending
                with lock:
                    pending_tasks.pop(task_id, None)
                print(f"Task failed: {task_id} — {data.get('request')}")


# Start fallback poller in background
poller_thread = threading.Thread(target=fallback_poller, daemon=True)
poller_thread.start()

JavaScript

const express = require("express");
const axios = require("axios");

const app = express();
const API_KEY = process.env.CAPTCHAAI_API_KEY;

const pendingTasks = new Map(); // taskId -> { submittedAt, status }
const results = new Map();

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

  if (resp.data.status === 1) {
    const taskId = resp.data.request;
    pendingTasks.set(taskId, {
      submittedAt: Date.now(),
      status: "pending",
    });
    return taskId;
  }
  return null;
}

// Primary callback endpoint
app.get("/callback", (req, res) => {
  const taskId = req.query.id;
  const solution = req.query.code;

  results.set(taskId, solution);
  pendingTasks.delete(taskId);

  res.sendStatus(200);
});

// Fallback poller
setInterval(async () => {
  const now = Date.now();
  const staleTasks = [];

  for (const [taskId, info] of pendingTasks) {
    if (now - info.submittedAt > 120000 && info.status === "pending") {
      staleTasks.push(taskId);
    }
  }

  for (const taskId of staleTasks) {
    try {
      const resp = await axios.get("https://ocr.captchaai.com/res.php", {
        params: { key: API_KEY, action: "get", id: taskId, json: 1 },
      });

      if (resp.data.status === 1) {
        results.set(taskId, resp.data.request);
        pendingTasks.delete(taskId);
        console.log(`Fallback recovered: ${taskId}`);
      } else if (resp.data.request !== "CAPCHA_NOT_READY") {
        pendingTasks.delete(taskId);
        console.log(`Task failed: ${taskId} — ${resp.data.request}`);
      }
    } catch (err) {
      console.error(`Poll error for ${taskId}: ${err.message}`);
    }
  }
}, 30000);

app.listen(3000);

Padrão 2: fila de mensagens mortas (dead-letter queue)

Se o seu handler recebe o callback mas quebra ao processá-lo — banco de dados fora do ar, falha de validação — não descarte o resultado. Grave-o numa fila de mensagens mortas e reprocesse depois.

Python

import json
import os
import time
from pathlib import Path

DEAD_LETTER_DIR = Path("dead_letter")
DEAD_LETTER_DIR.mkdir(exist_ok=True)


@app.route("/callback")
def captcha_callback_with_dlq():
    task_id = request.args.get("id")
    solution = request.args.get("code")

    try:
        # Attempt normal processing
        store_result(task_id, solution)
        return "OK", 200
    except Exception as e:
        # Processing failed — save to dead-letter queue
        dead_letter = {
            "task_id": task_id,
            "solution": solution,
            "error": str(e),
            "received_at": time.time()
        }
        dlq_path = DEAD_LETTER_DIR / f"{task_id}.json"
        dlq_path.write_text(json.dumps(dead_letter))

        print(f"DLQ: {task_id} — {e}")
        return "OK", 200  # Still return 200 to CaptchaAI


def reprocess_dead_letters():
    """Retry processing dead-letter items."""
    for dlq_file in DEAD_LETTER_DIR.glob("*.json"):
        item = json.loads(dlq_file.read_text())

        try:
            store_result(item["task_id"], item["solution"])
            dlq_file.unlink()  # Remove after successful processing
            print(f"DLQ reprocessed: {item['task_id']}")
        except Exception:
            pass  # Leave in DLQ for next retry

JavaScript

const fs = require("fs");
const path = require("path");

const DLQ_DIR = path.join(__dirname, "dead_letter");
if (!fs.existsSync(DLQ_DIR)) fs.mkdirSync(DLQ_DIR);

app.get("/callback-dlq", (req, res) => {
  const taskId = req.query.id;
  const solution = req.query.code;

  try {
    storeResult(taskId, solution);
    res.sendStatus(200);
  } catch (err) {
    // Save to dead-letter queue
    const deadLetter = {
      task_id: taskId,
      solution: solution,
      error: err.message,
      received_at: Date.now(),
    };

    fs.writeFileSync(
      path.join(DLQ_DIR, `${taskId}.json`),
      JSON.stringify(deadLetter)
    );

    console.log(`DLQ: ${taskId} — ${err.message}`);
    res.sendStatus(200); // Still acknowledge to CaptchaAI
  }
});

function reprocessDeadLetters() {
  const files = fs.readdirSync(DLQ_DIR).filter((f) => f.endsWith(".json"));

  for (const file of files) {
    const filePath = path.join(DLQ_DIR, file);
    const item = JSON.parse(fs.readFileSync(filePath, "utf8"));

    try {
      storeResult(item.task_id, item.solution);
      fs.unlinkSync(filePath);
      console.log(`DLQ reprocessed: ${item.task_id}`);
    } catch (err) {
      // Leave in DLQ
    }
  }
}

// Retry DLQ every 5 minutes
setInterval(reprocessDeadLetters, 300000);

Padrão 3: handler de callback idempotente

O mesmo callback pode chegar mais de uma vez — CaptchaAI pode reenviar em caso de dúvida sobre a entrega, e proxies/load balancers às vezes duplicam a requisição. Trate isso como algo normal, não como bug:

@app.route("/callback")
def idempotent_callback():
    task_id = request.args.get("id")
    solution = request.args.get("code")

    with lock:
        # Only process if not already handled
        if task_id in results:
            return "OK", 200  # Already processed — skip silently

        results[task_id] = solution
        pending_tasks.pop(task_id, None)

    return "OK", 200

Cenário: testando isso antes que aconteça em produção

Não espere a primeira queda real para descobrir se o fallback funciona:

  • Derrube o handler (kill) após submeter uma tarefa — o poller deve recuperar a solução na janela configurada.
  • Workers em sa-east-1? Meça a latência real até a CaptchaAI antes de fixar o tempo limite em 120 s.
  • Envie o mesmo id duas vezes manualmente e confirme que o handler idempotente ignora a segunda entrega.
  • Payload em log persistente segue a mesma retenção que dado de produção.

Resolução de problemas comuns

O poller de fallback encontra tarefas já entregues pelo callback

Corrida entre os dois. Adicione a checagem de idempotência do padrão 3.

A fila de mensagens mortas cresce e nunca esvazia

Reprocessador parado. Confira os logs e confirme que a causa raiz (banco fora do ar) foi corrigida.

O callback devolve 200, mas o resultado some

Handler quebra depois de enviar a resposta. Grave antes de responder "OK", ou use a fila de mensagens mortas.

Chegam muitas requisições de polling de fallback

Timeout do callback curto demais, ou instabilidade no próprio servidor. Aumente o limite e monitore o uptime.

Qual padrão usar: matriz de decisão

Cenário Padrão recomendado
Baixo volume, indisponibilidade ocasional Callback + polling de fallback
Alto volume, risco de queda do banco de dados Fila de mensagens mortas
Vários consumidores podem processar o mesmo resultado Handler idempotente
Sistema de produção com SLA Os três combinados

Na prática, a maioria dos times acaba implementando os três: polling cobre a entrega, DLQ cobre falhas de processamento, idempotência protege contra reentrega — cada um resolve um problema diferente.

Perguntas frequentes

O callback sempre precisa devolver HTTP 200 para a CaptchaAI?

Sim. Um código de erro (4xx/5xx) não ajuda — não há garantia de reentrega. Aceite sempre (200 OK) e trate falhas internamente com DLQ ou polling de fallback.

Por que o mesmo callback chega duas vezes no meu endpoint?

Esperado, não bug seu. Reentregas e duplicação por proxy acontecem em qualquer webhook — é para isso que serve o padrão 3.

O polling de fallback consome as threads do meu plano?

Não. As threads limitam tarefas em resolução simultânea, não consultas a res.php:

  • BASIC: 5 threads, US$ 15/mês.
  • ADVANCE: 50 threads, US$ 90/mês.

Um poller que só verifica tarefas atrasadas a cada 30 s tem impacto desprezível.

Preciso guardar os dados do callback por questão de compliance?

Se o payload tiver dado pessoal, trate-o como qualquer dado sensível sob a LGPD (Brasil) ou o RGPD (Portugal): defina um prazo de retenção e não guarde o payload bruto além do necessário. Orientação geral, não jurídica.

Leitura relacionada

Próximo passo

Blinde seu handler antes da próxima instabilidade de rede — crie sua chave de API na CaptchaAI e implemente os três padrões.

Guias relacionados

Os comentários estão desativados para este artigo.