In einer Progressive Web App steht das CAPTCHA nicht im ersten HTML – es wird erst per JavaScript ins DOM nachgeladen, oft Sekunden nach dem Seitenaufruf. Genau daran scheitern die meisten Automatisierungen: Sie suchen nach dem Sitekey, bevor das Widget existiert. Die Lösung: aktiv auf das dynamische Rendering warten, Sitekey und Page-URL zum richtigen Zeitpunkt auslesen und das Token über CaptchaAI lösen.
Dieser Leitfaden zeigt Erkennung, Extraktion und Lösung von reCAPTCHA v2 und Cloudflare Turnstile in PWAs – mit Playwright und alternativ mit Selenium in Python.
Warum sich CAPTCHAs in PWAs anders verhalten
Klassische Websites liefern das CAPTCHA direkt in der ersten HTML-Antwort. Eine PWA nicht – diese Unterschiede sind die Ursache für abbrechende Skripte:
| Aspekt | Klassische Website | PWA |
|---|---|---|
| CAPTCHA-Auslieferung | im initialen HTML | von JavaScript nach dem Laden gerendert |
| Navigation | vollständiger Page-Reload | clientseitiges Routing (kein Reload) |
| Service Worker | nicht vorhanden | cacht Ressourcen und kann Anfragen abfangen |
| DOM-Verfügbarkeit | sofort | erst nach dem Framework-Rendering |
| Netzwerkanfragen | direkt | können vom Service Worker abgefangen werden |
PWA-Problem oder CAPTCHA-Problem? Schnelle Diagnose
Klären Sie zuerst, ob der Solver das Problem ist – oder der Zeitpunkt, zu dem Ihr Skript zugreift:
| Beobachtung | Wahrscheinlichste Ursache |
|---|---|
| Im initialen HTML kein CAPTCHA, später aber schon | Das Widget wird erst clientseitig gerendert |
| Nach einem In-App-Wechsel erscheint plötzlich ein CAPTCHA | Routing läuft ohne echten Page-Reload |
| Die Sitekey-Extraktion klappt nur sporadisch | Der Auslesezeitpunkt passt nicht zum Framework-Lifecycle |
Praxisbeispiel: Turnstile in einem Shopware-PWA-Storefront
Ein Szenario aus dem DACH-Raum: Ein Shopware-6-Shop wird als PWA-Storefront ausgeliefert und schützt den Login mit Cloudflare Turnstile. Beim ersten Rendern der App-Shell ist das Widget noch nicht im DOM – ein Skript, das sofort nach .cf-turnstile sucht, bricht ab. Erst nach dem Mounten der Frontend-Komponente erscheint der Container mit dem data-sitekey. Wer auf das Element wartet statt auf den load-Event, löst das Problem in einer Zeile – dasselbe Muster gilt für JTL-Frontends und für Termin- und Behördenportale.
Für ein stabiles Skript in diesem Setup kommt es auf drei Punkte an:
- auf das gemountete Widget warten, nicht auf den
load-Event des Dokuments data-sitekeyund Page-URL erst nach dem Rendern der Komponente auslesen- nach jedem clientseitigen Routenwechsel erneut auf ein CAPTCHA prüfen
Hinweis zur Sorgfaltspflicht: Beim automatisierten Abfragen fallen IP-Adressen an – nach DSGVO personenbezogene Daten. Prüfen Sie Rechtsgrundlage und Datenflüsse, bevor Sie produktiv gehen.
Schritt 1: Auf das dynamische CAPTCHA-Rendering warten
Der häufigste Fehler: den Sitekey auslesen, bevor das Framework das Widget gerendert hat. Warten Sie aktiv auf das Element – per MutationObserver oder Framework-Signal:
// pwa_captcha_detector.js — Playwright script
const { chromium } = require('playwright');
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
async function detectCaptchaInPWA(page) {
// Wait for the PWA app shell to render
await page.waitForLoadState('networkidle');
// Use MutationObserver to detect dynamically loaded CAPTCHAs
const captchaInfo = await page.evaluate(() => {
return new Promise((resolve) => {
// Check if CAPTCHA is already present
const existing = document.querySelector('.g-recaptcha, .cf-turnstile');
if (existing) {
resolve({
type: existing.classList.contains('g-recaptcha')
? 'recaptcha_v2' : 'turnstile',
sitekey: existing.getAttribute('data-sitekey'),
pageurl: window.location.href,
});
return;
}
// Watch for CAPTCHA elements added dynamically
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
const captcha = node.matches?.('.g-recaptcha, .cf-turnstile')
? node
: node.querySelector?.('.g-recaptcha, .cf-turnstile');
if (captcha) {
observer.disconnect();
resolve({
type: captcha.classList.contains('g-recaptcha')
? 'recaptcha_v2' : 'turnstile',
sitekey: captcha.getAttribute('data-sitekey'),
pageurl: window.location.href,
});
return;
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
// Timeout after 15 seconds
setTimeout(() => {
observer.disconnect();
resolve(null);
}, 15000);
});
});
return captchaInfo;
}
async function main() {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example-pwa.com/login');
const captcha = await detectCaptchaInPWA(page);
if (!captcha) {
console.log('No CAPTCHA detected');
await browser.close();
return;
}
console.log(`Detected ${captcha.type}: ${captcha.sitekey}`);
// Mit CaptchaAI lösen
const token = await solveCaptcha(captcha);
console.log(`Token: ${token.substring(0, 50)}...`);
// Inject token
await injectToken(page, captcha.type, token);
// Submit form
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle' });
console.log('Form submitted');
await browser.close();
}
async function solveCaptcha(captcha) {
const params = {
key: API_KEY,
pageurl: captcha.pageurl,
json: '1',
};
if (captcha.type === 'recaptcha_v2') {
params.method = 'userrecaptcha';
params.googlekey = captcha.sitekey;
} else {
params.method = 'turnstile';
params.sitekey = captcha.sitekey;
}
const submit = await axios.get(
'https://ocr.captchaai.com/in.php', { params }
);
if (submit.data.status !== 1) throw new Error(submit.data.request);
const taskId = submit.data.request;
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: '1' },
});
if (poll.data.status === 1) return poll.data.request;
if (poll.data.request !== 'CAPCHA_NOT_READY') {
throw new Error(poll.data.request);
}
}
throw new Error('Timeout');
}
async function injectToken(page, type, token) {
if (type === 'recaptcha_v2') {
await page.evaluate((t) => {
document.getElementById('g-recaptcha-response').value = t;
try {
const clients = ___grecaptcha_cfg.clients;
Object.keys(clients).forEach((k) => {
Object.keys(clients[k]).forEach((j) => {
if (clients[k][j]?.callback) clients[k][j].callback(t);
});
});
} catch (e) {}
}, token);
} else {
await page.evaluate((t) => {
const input = document.querySelector('[name="cf-turnstile-response"]');
if (input) input.value = t;
const cb = document.querySelector('.cf-turnstile')
?.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') window[cb](t);
}, token);
}
}
main().catch(console.error);
Schritt 2: Veralteten Service-Worker-Cache behandeln
Service Worker cachen CAPTCHA-Skripte mitunter aggressiv – das führt zu veralteten Widgets. Umgehen Sie den Cache gezielt für die betroffenen Ressourcen:
// Intercept and bypass Service Worker cache for CAPTCHA scripts
await page.route('**/recaptcha/**', (route) => {
route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});
await page.route('**/turnstile/**', (route) => {
route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});
Schritt 3: Clientseitiges Routing überwachen
PWAs navigieren clientseitig – der Aufruf einer geschützten Route löst keinen klassischen Seitenladevorgang aus. Überwachen Sie Routenwechsel und prüfen danach erneut auf ein CAPTCHA:
// Monitor PWA route changes for new CAPTCHAs
await page.evaluate(() => {
const originalPushState = history.pushState;
history.pushState = function() {
originalPushState.apply(this, arguments);
window.dispatchEvent(new Event('pwa-route-change'));
};
});
page.on('console', async (msg) => {
// React to route changes if needed
});
// Or wait for specific route
await page.waitForURL('**/checkout', { waitUntil: 'networkidle' });
// Then detect CAPTCHA on the new route
const captcha = await detectCaptchaInPWA(page);
Schritt 4: Python-Alternative mit Selenium
Dieselbe Logik funktioniert in Python mit Selenium – praktisch, wenn Ihre Test-Pipeline ohnehin darauf aufsetzt. Entscheidend bleibt das explizite Warten auf das Widget:
# pwa_captcha_selenium.py
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://example-pwa.com/login")
# Wait for PWA to render CAPTCHA
wait = WebDriverWait(driver, 20)
captcha_el = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".g-recaptcha, .cf-turnstile"))
)
sitekey = captcha_el.get_attribute("data-sitekey")
pageurl = driver.current_url
is_turnstile = "cf-turnstile" in captcha_el.get_attribute("class")
# Submit to CaptchaAI
params = {"key": API_KEY, "pageurl": pageurl, "json": "1"}
if is_turnstile:
params["method"] = "turnstile"
params["sitekey"] = sitekey
else:
params["method"] = "userrecaptcha"
params["googlekey"] = sitekey
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
task_id = resp.json()["request"]
# Poll
for _ in range(30):
time.sleep(5)
poll = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": "1",
})
if poll.json().get("status") == 1:
token = poll.json()["request"]
break
else:
raise TimeoutError("CAPTCHA not solved")
# Inject token
driver.execute_script(f"""
document.getElementById('g-recaptcha-response').value = '{token}';
""")
driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()
print("Form submitted")
driver.quit()
Fehlerbehebung
| Problem | Ursache | Lösung |
|---|---|---|
| CAPTCHA-Element taucht nie auf | Route noch nicht fertig gerendert | waitForSelector mit großzügigem Timeout; Routing muss abgeschlossen sein |
| Sitekey nach Navigation veraltet | Service Worker liefert gecachtes HTML | Cache-Header für CAPTCHA-Ressourcen umgehen (Schritt 2) |
| Token erzeugt, aber vom Ziel abgelehnt | Sitekey, Page-URL oder Session passen nicht | Parameter neu erfassen, Token in derselben Sitzung nutzen |
| Formular sendet Token nicht mit | SPA-Handler liest aus dem Komponenten-State, nicht aus dem DOM | Zusätzlich den Framework-State setzen (React-Ref, reaktive Vue-Property) |
Häufige Fragen
Warum wird der Sitekey in meiner PWA nur sporadisch gefunden?
Weil das Widget zum Auslesezeitpunkt noch nicht im DOM liegt. Das Framework rendert es erst nach dem Mounten der Komponente. Warten Sie mit waitForSelector oder einem MutationObserver aktiv auf das Element – nicht auf DOMContentLoaded und nie auf ein festes sleep.
Wie erkenne ich in einer Single-Page-App den Wechsel zu einer CAPTCHA-Route?
Über die History-API. Da clientseitiges Routing keinen echten Seitenaufruf auslöst, hängen Sie sich an history.pushState oder nutzen page.waitForURL(...) und starten die Erkennung nach jedem Routenwechsel neu.
Kann ich in einer PWA mehrere CAPTCHAs gleichzeitig lösen?
Ja, begrenzt durch die Threads Ihres Plans. CaptchaAI rechnet Thread-basiert mit unbegrenzten Lösungen pro Thread – BASIC (15 $/Monat, 5 Threads) erlaubt fünf gleichzeitige Lösungen, ADVANCE (90 $/Monat, 50 Threads) entsprechend mehr.
Playwright oder Selenium – was passt besser für PWAs?
Beide funktionieren mit demselben Erkennungsmuster. Playwright bringt mit waitForURL und dem Request-Routing etwas mehr Komfort für clientseitiges Routing mit. Selenium ist die pragmatische Wahl, wenn Ihre Test-Pipeline ohnehin darauf aufsetzt – entscheidend bleibt in beiden Fällen das explizite Warten auf das Widget.
Reicht ein festes Sleep, um auf das Widget zu warten?
Nein. Ein festes sleep ist entweder zu kurz – dann fehlt das Widget noch – oder unnötig langsam. Warten Sie ereignisbasiert mit waitForSelector oder einem MutationObserver auf das konkrete Element; das ist schneller und deutlich stabiler.
Fazit
CAPTCHAs in PWAs sind kein grundsätzlich anderes Problem – nur ein Timing-Problem. Wer aktiv auf das gerenderte Widget wartet, Sitekey und Page-URL zum richtigen Zeitpunkt ausliest und clientseitige Routenwechsel überwacht, löst reCAPTCHA v2 und Turnstile in einer PWA genauso zuverlässig wie auf einer klassischen Seite. Die eigentliche Lösung übernimmt anschließend die CaptchaAI-API.
Verwandte Leitfäden
- CAPTCHA-Handling im Shadow DOM von Web Components
- CAPTCHAs in React-Native-WebViews lösen
- Browser-Automatisierung mit CAPTCHA auf Mobilgeräten