Dans une SPA, le sitekey n'est pas dans la source de la page : il arrive avec le bundle JavaScript, et le widget n'apparaît qu'au montage du composant.
La séquence qui fonctionne tient en trois temps :
- Attendre le rendu du widget.
- Extraire le sitekey (iframe ou
data-sitekey). - Injecter le token juste avant l'envoi.
Ce qui change par rapport à une page rendue côté serveur
| Page classique | SPA |
|---|---|
| Script reCAPTCHA dans le HTML | Script injecté au changement de route |
| Widget rendu au chargement | Widget rendu au montage |
| Sitekey dans la source | Sitekey dans le bundle JavaScript |
| Formulaire envoyé en POST | Formulaire envoyé en XHR/fetch |
- Sans le clic qui ouvre le formulaire React, l'iframe est absente.
- Le
v-ifd'un tunnel Vue remonte le widget entre deux étapes. - L'envoi part en XHR, sans rechargement à observer.
Repérer le widget reCAPTCHA rendu par la SPA
Attendre l'apparition de l'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://example.com/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"))
Trois endroits où chercher le sitekey
- L'URL de l'iframe : le paramètre
k=porte la clé. - L'attribut
data-sitekey, dès le montage du composant. - Le bundle : dernier recours, à revalider à chaque build.
# 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;
}
""")
Envoyer la tâche à l'API CaptchaAI
pageurlporte l'URL affichée après la navigation interne, pas l'URL d'entrée.- Le sitekey extrait plus haut part dans
googlekey. - Le résultat arrive par polling sur
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")
Injecter le token reCAPTCHA là où la SPA le lit
- Zone de texte
g-recaptcha-response: relue à l'envoi. - Callback
data-callback: React ou Vue pilote l'état. - Requête XHR : l'envoi passe par une API REST.
Option 1 : remplir la zone de texte masquée
Cliquez dans la foulée : un re-rendu vide la zone de texte.
token = solve_recaptcha(site_key, "https://example.com/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")
Option 2 : appeler le callback déclaré par le widget
Méthode la plus fiable dès que React ou Vue contrôle le formulaire.
Le callback met à jour l'état du composant, pas seulement le DOM.
# 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'));
}}
""")
Option 3 : intercepter la requête XHR avec Puppeteer
Vérifiez ainsi que le token voyage bien dans le 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://example.com/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);
}
Particularités React, Vue et Angular
React (react-google-recaptcha)
Le paquet react-google-recaptcha monte le widget de façon asynchrone.
Visez l'état attached : le conteneur reste parfois masqué.
# 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 (vue-recaptcha)
Le widget dépend souvent d'un v-if : déclenchez l'étape d'abord.
# 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
Les modules reCAPTCHA arrivent souvent en lazy-load : visez plusieurs sélecteurs.
# Angular apps may lazy-load reCAPTCHA modules
# Wait for the specific Angular component
page.wait_for_selector("re-captcha, app-recaptcha, [data-sitekey]")
Navigation interne : quand la route change sans rechargement
- Le changement de route ne déclenche ni
loadniDOMContentLoaded. - Toute attente basée sur la navigation échoue silencieusement.
- Attendez le sélecteur, pas la page.
# 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)
Cas concret : un tunnel de réservation testé en préproduction
Une équipe QA à Lyon automatise un tunnel de réservation Nuxt hébergé chez OVHcloud. Le reCAPTCHA v2 n'apparaît qu'à la troisième étape, et chaque retour arrière remonte le composant.
- Clic sur « Confirmer », puis attente du sélecteur.
- Extraction du sitekey, envoi à l'API.
- Injection par callback, puis envoi immédiat.
Une suite nocturne de ce type tient sur le plan BASIC ($15/mois, 5 threads), facturé en dollars US.
Côté RGPD, purgez les logs de test.
Dépannage
Cinq symptômes reviennent presque toujours.
| Problème | Cause | Correctif |
|---|---|---|
Zone de texte g-recaptcha-response absente |
Composant non monté | wait_for_selector, timeout 10–15 s |
| Token injecté, formulaire immobile | Callback jamais appelé | Lire et appeler data-callback |
| Sitekey absent de la source | Clé dans le bundle | Attendre le rendu, lire l'iframe |
| Token valide une fois puis rejeté | Composant remonté | Injecter juste avant l'envoi |
| Valeur injectée effacée aussitôt | Réactivité React/Vue | Passer par data-callback |
FAQ
Pourquoi le token disparaît-il de g-recaptcha-response juste après l'injection ?
Parce que le framework a re-rendu le composant.
React et Vue considèrent le DOM comme leur propriété : passez par le callback.
Combien de temps un token reCAPTCHA reste-t-il exploitable ?
Environ deux minutes côté Google.
Lancez la résolution quand le widget s'affiche, pas au début du scénario.
Le sitekey change-t-il d'une route à l'autre ?
Rarement, mais c'est possible selon l'environnement ou la route.
Relisez-le à chaque étape plutôt qu'une fois pour toute la session.
Quel timeout choisir pour l'attente du widget ?
10 à 15 secondes couvrent la plupart des montages.
En dessous, vous mesurez surtout la latence réseau.
CaptchaAI prend-il en charge hCaptcha si ma SPA en utilise un ?
Non — pas encore pris en charge, pas plus que FunCaptcha (Arkose Labs).
Types couverts : reCAPTCHA v2 et v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, CAPTCHA image/OCR.
GeeTest v4 est à venir ; CaptchaFox, Friendly Captcha et Lemin sont en bêta.
Articles connexes
- Déclencher le callback reCAPTCHA v2 via l'API
- Gérer reCAPTCHA v2 et Turnstile sur un même site
- Comprendre le mécanisme de callback reCAPTCHA v2
Prochaines étapes
Rejouez la séquence détection → résolution → injection sur votre SPA : récupérez votre clé API CaptchaAI.