Tutorials

reCAPTCHA em aplicativos de página única: padrões de carregamento dinâmico

Se o teste falha na linha que procura o g-recaptcha-response, o seletor está certo: o elemento é que ainda não existe. Em React, Vue ou Angular o widget nasce por JavaScript na montagem do componente, às vezes só após um clique ou uma troca de rota.

São quatro passos: esperar o widget, achar a sitekey, pedir o token à API e entregá-lo no ponto certo.

O que muda no reCAPTCHA dentro de um SPA

Página tradicional SPA
script no HTML inicial script injetado na troca de rota
widget no load widget na montagem do componente
sitekey no código-fonte sitekey no bundle JS
envio por POST envio por XHR/fetch

Nada disso muda o desafio, só o instante em que ele existe.

O custo disso em uma suíte de QA

Um time em São Paulo roda 40 cenários ponta a ponta em staging.example.com a cada deploy, numa loja em Vue com reCAPTCHA v2 no checkout. Em série, só os desafios já estouram a janela noturna.

Paralelize por thread: no BASIC (US$ 15/mês, 5 threads) cinco cenários avançam juntos; no STANDARD (US$ 30/mês, 15 threads) a suíte cabe em três ondas. A cobrança é por thread simultânea, com resoluções ilimitadas: rodar duas vezes ao dia não muda a fatura. Mantenha tudo em ambiente próprio e com dados fictícios — dado pessoal real traz a LGPD junto.

Passo 1: espere o widget do reCAPTCHA renderizar

Nada de sleep fixo: o tempo de montagem varia com a rede e com o code splitting. Dispare a ação que cria o widget e espere o iframe.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto("https://staging.example.com/qa-login")

    # SPA may need a click or navigation to trigger reCAPTCHA
    page.click("#show-login-form")

    # Wait for reCAPTCHA iframe to appear
    recaptcha_frame = page.wait_for_selector(
        "iframe[src*='recaptcha']",
        timeout=15000
    )
    print("reCAPTCHA detected:", recaptcha_frame.get_attribute("src"))

Passo 2: descubra onde está a sitekey

Ela quase nunca está no HTML inicial. Procure nesta ordem:

  • no src do iframe, no parâmetro k=;
  • no atributo data-sitekey do container renderizado;
  • no bundle JavaScript, como último recurso.
# Method 1: From the iframe src
iframe_src = recaptcha_frame.get_attribute("src")
# src contains: ...?k=6LcR_RsTAAAAADge...
import re
match = re.search(r'[?&]k=([^&]+)', iframe_src)
site_key = match.group(1) if match else None

# Method 2: From data-sitekey attribute
site_key = page.eval_on_selector(
    "[data-sitekey]",
    "el => el.getAttribute('data-sitekey')"
)

# Method 3: From JavaScript bundle (last resort)
site_key = page.evaluate("""
    () => {
        // Check for grecaptcha render calls
        const scripts = document.querySelectorAll('script');
        for (const s of scripts) {
            const match = s.textContent.match(/sitekey['":\s]+(['"])(6L[^'"]+)\\1/);
            if (match) return match[2];
        }
        return null;
    }
""")

Passo 3: envie a sitekey à API da CaptchaAI

Daqui em diante o framework não importa: a CaptchaAI recebe method=userrecaptcha, googlekey e pageurl em in.php e devolve o token em res.php.

import requests
import time

def solve_recaptcha(site_key, page_url):
    # Submit task
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": "YOUR_API_KEY",
        "method": "userrecaptcha",
        "googlekey": site_key,
        "pageurl": page_url,
        "json": 1
    })
    task_id = resp.json()["request"]

    # Poll for result
    for _ in range(60):
        time.sleep(3)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": "YOUR_API_KEY",
            "action": "get",
            "id": task_id,
            "json": 1
        })
        data = result.json()
        if data["status"] == 1:
            return data["request"]
    raise TimeoutError("Solve timed out")

Passo 4: entregue o token onde o SPA espera

Um SPA não faz POST de formulário: chama uma API. O token precisa chegar onde o framework o lê.

Opção 1: preencher o textarea oculto

Serve quando o widget renderiza uma vez só e nada reage àquele campo.

token = solve_recaptcha(site_key, "https://staging.example.com/qa-login")

# Inject into the g-recaptcha-response textarea
page.evaluate(f"""
    document.getElementById('g-recaptcha-response').value = '{token}';
""")

# Submit the form
page.click("#login-button")

Opção 2: chamar o callback registrado

O caminho mais confiável em React e Vue: chame a função de data-callback em vez de escrever num campo que o framework limpa.

# Find the callback function name
callback = page.evaluate("""
    () => {
        const widget = document.querySelector('.g-recaptcha');
        return widget?.getAttribute('data-callback') || null;
    }
""")

# Call it with the token
if callback:
    page.evaluate(f"window['{callback}']('{token}')")
else:
    # Try the default grecaptcha callback
    page.evaluate(f"""
        if (window.grecaptcha) {{
            // Set the response and trigger verification
            document.getElementById('g-recaptcha-response').value = '{token}';
            // Find and call any registered callbacks
            const form = document.querySelector('form');
            if (form) form.dispatchEvent(new Event('submit'));
        }}
    """)

Opção 3: acompanhar a requisição XHR no Puppeteer

Quando o envio sai por fetch para um endpoint REST, confirme que o token viajou no payload.

const puppeteer = require('puppeteer');

async function solveRecaptchaInSPA() {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  // Intercept the API call and inject the token
  await page.setRequestInterception(true);
  page.on('request', request => {
    if (request.url().includes('/api/login')) {
      const postData = JSON.parse(request.postData() || '{}');
      // Token already set via page.evaluate — let it pass
      request.continue();
    } else {
      request.continue();
    }
  });

  await page.goto('https://staging.example.com/qa-login');
  await page.waitForSelector('[data-sitekey]');

  const siteKey = await page.$eval(
    '[data-sitekey]',
    el => el.getAttribute('data-sitekey')
  );

  // Solve with CaptchaAI (implementation omitted — same as Python)
  const token = await solveCaptcha(siteKey, page.url());

  // Inject token and trigger callback
  await page.evaluate((t) => {
    document.getElementById('g-recaptcha-response').value = t;
    const callback = document.querySelector('.g-recaptcha')
      ?.getAttribute('data-callback');
    if (callback && window[callback]) {
      window[callback](t);
    }
  }, token);
}

React: espere o componente montar

Com react-google-recaptcha a montagem é assíncrona: espere o .g-recaptcha ficar anexado.

# Wait for React to mount the component
page.wait_for_selector(".g-recaptcha", state="attached")

# The sitekey is in the rendered div's data attribute
site_key = page.eval_on_selector(
    ".g-recaptcha", "el => el.dataset.sitekey"
)

Vue: o v-if adia a renderização do widget

Com vue-recaptcha sob um v-if, o widget só existe depois da interação que satisfaz a condição.

# Vue may use v-if to conditionally render
# Navigate or interact to trigger the condition
page.click("#proceed-to-checkout")
page.wait_for_selector("iframe[src*='recaptcha']")

Angular: o lazy loading atrasa o widget

Módulos sob demanda adiam a renderização: aceite mais de um seletor.

# Angular apps may lazy-load reCAPTCHA modules
# Wait for the specific Angular component
page.wait_for_selector("re-captcha, app-recaptcha, [data-sitekey]")

Troca de rota sem recarregar a página

A URL muda sem recarregar e nenhum evento de load avisa a automação: espere o iframe a cada navegação interna.

# Listen for reCAPTCHA appearing after SPA navigation
page.goto("https://example.com")

# Navigate within the SPA
page.click("a[href='/login']")

# The URL changed but no page reload happened
# Wait for the CAPTCHA to render in the new "page"
page.wait_for_selector("iframe[src*='recaptcha']", timeout=10000)

Erros comuns ao resolver reCAPTCHA em SPA

Sintoma Causa Correção
g-recaptcha-response ausente componente não montou wait_for_selector com timeout folgado
token no campo, nada é enviado callback não foi chamado chame a função de data-callback
sitekey fora do código-fonte está no bundle leia o src do iframe
funciona uma vez, falha depois re-render limpa o campo escreva o token junto ao envio
widget volta ao início reatividade sobrescreve o valor prefira data-callback ao textarea

Perguntas frequentes

O token expira enquanto o componente re-renderiza?

Pode. O token vale 120 segundos e aceita uma única validação: resolva perto do envio.

A sitekey muda de uma rota para outra?

Costuma ser a mesma, mas login e checkout podem carregar widgets diferentes. Leia a sitekey em cada rota.

Quantas threads preciso para rodar cenários em paralelo?

Uma por cenário simultâneo. O BASIC (US$ 15/mês) entrega 5 threads e o ADVANCE (US$ 90/mês), 50. Com resoluções ilimitadas, você dimensiona simultaneidade, não volume.

E se o SPA usar hCaptcha no lugar do reCAPTCHA?

Não dá: o hCaptcha não é suportado, nem o FunCaptcha. A cobertura inclui reCAPTCHA v2 e v3, Cloudflare Turnstile e Challenge, GeeTest v3 e CAPTCHAs de imagem e grade.

Artigos relacionados

Próximas etapas

Rode os quatro passos no seu ambiente de homologação: pegue sua chave de API da CaptchaAI.

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