Un test qui passe le matin et échoue l'après-midi a rarement un problème de CAPTCHA : il a un problème d'horloge. Le token est arrivé, mais le backend l'a vérifié après la fin de sa fenêtre de validité, sans horodatage pour le prouver. Redis règle ce point : trois champs datés par exécution, une expiration native sur la clé.
Périmètre : ces schémas de diagnostic sont destinés à vos propres environnements QA ou à des environnements explicitement autorisés.
Ce que la durée de vie d'un token change en QA
Les fenêtres de validité sont courtes — de l'ordre de deux minutes pour reCAPTCHA, environ cinq minutes pour Cloudflare Turnstile — et tout ce qui s'intercale avant l'appel de vérification consomme cette marge : un sleep dans une fixture, un runner CI saturé, une capture d'écran.
Un résultat est à usage unique et lié à sa page : l'enjeu n'est pas de le conserver, mais de répondre à quatre questions à chaque exécution — quand la tâche est partie, quand le résultat est revenu, quelle marge restait, ce qu'a répondu le backend.
Le schéma TTL minimal à écrire dans Redis
Sept champs suffisent. Ne stockez jamais le token complet : un préfixe identifie l'enregistrement sans garder de secret exploitable, minimisation RGPD comprise.
| Champ | Rôle | Exemple |
|---|---|---|
task_id |
Tâche CaptchaAI | 185734920 |
issued_at |
Création de la tâche | 2026-04-27T08:15:11Z |
received_at |
Arrivée du résultat | 2026-04-27T08:15:29Z |
expires_at |
Borne d'expiration QA | 2026-04-27T08:17:09Z |
test_run_id |
Exécution de test | checkout-regression-1042 |
environment |
Environnement | staging |
verification_result |
Verdict du backend | accepted / expired / invalid |
Étape 1 : écrire l'enregistrement à la réception du résultat
La clé porte l'identifiant de l'exécution et son ex reprend la fenêtre à tester : Redis expire l'enregistrement en même temps que le token, et TTL <clé> donne la marge restante en secondes.
from __future__ import annotations
from datetime import datetime, timedelta, timezone
import json
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
def token_record_key(test_run_id: str) -> str:
return f"qa:captcha:ttl:{test_run_id}"
def store_ttl_record(*, test_run_id: str, task_id: str, received_token: str, ttl_seconds: int) -> str:
now = datetime.now(timezone.utc)
payload = {
"task_id": task_id,
"token_preview": received_token[:16],
"issued_at": now.isoformat(),
"expires_at": (now + timedelta(seconds=ttl_seconds)).isoformat(),
"verification_result": "pending",
"environment": "staging",
}
key = token_record_key(test_run_id)
r.set(key, json.dumps(payload), ex=ttl_seconds)
return key
Étape 2 : rattacher la résolution CaptchaAI à l'exécution
Gardez les deux valeurs de retour : c'est le couple task_id + résultat qui rend la mesure corrélable.
import requests
import time
API_KEY = "YOUR_API_KEY"
def submit_and_poll(page_url: str, sitekey: str) -> tuple[str, str]:
submit = requests.post(
"https://ocr.captchaai.com/in.php",
data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": 1,
},
timeout=30,
)
submit.raise_for_status()
submit_json = submit.json()
if submit_json.get("status") != 1:
raise RuntimeError(submit_json.get("request", "submit failed"))
task_id = submit_json["request"]
for _ in range(30):
time.sleep(5)
result = requests.get(
"https://ocr.captchaai.com/res.php",
params={"key": API_KEY, "action": "get", "id": task_id, "json": 1},
timeout=30,
)
result.raise_for_status()
result_json = result.json()
if result_json.get("status") == 1:
return task_id, result_json["request"]
raise TimeoutError("CaptchaAI polling timed out")
Le plafond de la boucle est une donnée : si vos exécutions le frôlent, la marge est déjà consommée. Regardez alors les threads de votre plan — BASIC ($15/mois, 5 threads) sature vite en parallèle.
Étape 3 : pousser le backend jusqu'à la borne d'expiration
Une exécution utile fait deux passages : une vérification immédiate, puis une vérification retardée après la borne interne.
def verify_in_staging(token: str, test_run_id: str, delay_seconds: int = 0) -> dict:
if delay_seconds:
time.sleep(delay_seconds)
response = requests.post(
"https://staging.example-app.test/qa/captcha/verify",
json={
"token": token,
"testRunId": test_run_id,
"environment": "staging",
},
timeout=30,
)
response.raise_for_status()
return response.json()
C'est le second passage qui trouve les bugs : un backend qui accepte un token trente secondes après la borne ne valide plus rien. Écrivez le verdict dans verification_result avant l'expiration de la clé.
Cas pratique : une régression de checkout côté CI
Une équipe belge exécute sa suite sur un staging hébergé en Europe. En local tout passe ; sur le runner CI, un paiement sur trois est refusé. Les enregistrements TTL tranchent : la marge restante tombe d'environ 90 s en local à moins de 10 s sur le runner, où le CI insère une capture Playwright avant la vérification. Le correctif ne touche ni CaptchaAI ni Redis : la capture passe après.
Dépannage
| Problème | Cause | Correctif |
|---|---|---|
| Chronologie d'un échec impossible à reconstituer | Horodatages manquants ou horloges divergentes | Écrire issued_at, received_at, expires_at en UTC |
| Clés disparues avant la fin du test | ex calé sans marge |
Allonger la fenêtre QA interne |
| Verdicts opposés sur deux exécutions | Fixtures divergentes | Journaliser test_run_id et environment |
| Redis dit « accepted », le produit échoue | Traces non corrélées | Propager le même identifiant jusqu'au backend |
| Marge restante proche de zéro | sleep fixes ou threads saturés |
Retirer les attentes inutiles, vérifier les threads |
Questions fréquentes
Combien de temps un token reste-t-il exploitable ?
Quelques minutes au plus, selon le type. Ne figez pas ces valeurs dans vos assertions : mesurez la marge observée et testez autour.
Faut-il stocker le token complet dans Redis ?
Non. Un préfixe suffit à identifier l'enregistrement, et un résultat CAPTCHA reste un secret de courte durée, à tenir hors des logs.
Que faire si le backend accepte un token expiré ?
Traitez-le comme un défaut bloquant : reproduisez-le avec un délai fixe dans verify_in_staging, puis contrôlez la réponse de l'API de vérification.
Cette méthode couvre-t-elle hCaptcha ?
Non : hCaptcha et FunCaptcha ne sont pas pris en charge, GeeTest v4 est à venir. Elle couvre reCAPTCHA v2 et v3, Cloudflare Turnstile, GeeTest v3 et les CAPTCHA image ou texte.
Guides connexes sûrs
- le démarrage rapide de l'API
- les tests QA autorisés
- tester vos endpoints de formulaire
- le branchement sur l'intégration continue
Ouvrez un compte CaptchaAI et mesurez la marge d'expiration réelle sur votre staging dès la prochaine exécution.